Every release, reconstructed from the project's own CHANGELOG.md. Full detail on GitHub. Back to the command reference.
Bisher wurden alle AGs der Instanz verarbeitet, auch die, in denen sie nur Secondary ist. Dort
haette Remove-DbaAgDatabase die lokale Kopie aus der AG genommen und das anschliessende
Add-DbaAgDatabase waere gescheitert, die Datenbank waere auf diesem Knoten draussen geblieben.
Secondary-AGs werden jetzt uebersprungen und protokolliert (mit Nennung des tatsaechlichen
Primary). Auch Invoke-sqmSqlAlwaysOnAutoseeding wird nur noch fuer die Primary-AGs aufgerufen
statt mit -All. Laeuft der Job auf allen Knoten, repariert jeder die AGs, deren Primary er ist.
Das Zwei-Schritt-Beispiel uebergab Repair-sqmAlwaysOnDatabases den Parameter
AvailabilityGroupName, den die Funktion nicht hat. Ein so angelegter Job waere am
Parameter-Binding gescheitert. Ersetzt durch NoReport = $true.
-AvailabilityGroup ist optional. Ohne Angabe werden die AGs ermittelt, in denen die Instanz
Primary ist. Genau eine: wird verwendet. Mehrere: nummerierte Liste mit Rueckfrage (leer = Abbruch).
In einer nicht-interaktiven Sitzung (Agent-Job, -NonInteractive) wird nicht gefragt, sondern mit
der Liste der AG-Namen abgebrochen. Ist die Instanz nirgends Primary, nennt der Fehler den
tatsaechlichen Primary je AG.SET RECOVERY FULL. Bis zum ersten FULL-Backup fehlt die Log-Chain
("pseudo-simple") und Add-DbaAgDatabase lehnt die Datenbank ab. Das Backup laeuft jetzt direkt
nach der Umstellung, und ebenso fuer Datenbanken, die schon Full sind, aber noch nie danach
gesichert wurden (last_log_backup_lsn ist NULL). Ziel: Standard-Backupverzeichnis der Instanz
oder neu -BackupPath. Schlaegt das Backup fehl (Status BackupFailed), wird auf den Secondaries
nichts geloescht und nichts hinzugefuegt.Set-DbaDbRecoveryModel laeuft jetzt mit -Confirm:$false -EnableException: vorher konnte es
nachfragen, und ein Fehler kam wegen -ErrorAction Stop ohne -EnableException nicht im catch an.Test-sqmInteractiveSession.Select-Object -ExpandProperty name -Join ',' ist kein gueltiger Parameter
(ParameterBindingException "Join"). Tritt genau dann auf, wenn die Instanz AG-Mitglied ist, und
verdeckte damit die eigentliche Meldung. Jetzt (...) -join ',', die AG-Meldung erscheint korrekt.GetNewClosure() erfasst nur lokale Variablen, Farbpalette und $styleButton lagen im
Funktions-Scope und waren im Click-Handler $null. Werden jetzt vor dem Handler lokal kopiert.Fuenf Schritte pro Benutzerdatenbank, jeder einzeln abschaltbar (-Skip...), mit -WhatIf und
CSV-Protokoll:
| Schritt | Was passiert |
|---|---|
| FixOrphanUser | User ohne passende SID, aber mit gleichnamigem Login: ALTER USER ... WITH LOGIN |
| RemoveUserWithoutLogin | Uebrige User ohne Login: besessene Schemas/Rollen an dbo, dann DROP USER (eine Transaktion) |
| CompatibilityLevel | Auf die Stufe der Serverversion anheben (2022 = 160), nie absenken |
| TargetRecoveryTime | TARGET_RECOVERY_TIME = 60 SECONDS (einstellbar) |
| DatabaseOwner | sa, ermittelt ueber SID 0x01 (funktioniert auch nach Umbenennung) |
Nicht entfernt werden: User WITHOUT LOGIN (Service Broker, Signaturen; nur mit
-IncludeUsersWithoutLogin), Contained User, Zertifikats-User, interne ##...##-User und
Windows-User, die keinen eigenen Login haben, aber ueber einen Gruppen-Login auf den Server kommen.
Letzteres prueft xp_logininfo; ohne die Pruefung wuerde man einem funktionierenden Konto den
Datenbankzugriff wegnehmen. Offline-, Read-only- und AG-Secondary-Datenbanken werden uebersprungen.
Die bisherigen Schritte 7-9 (Repair-DbaDbOrphanUser, Windows-User ohne Login entfernen, Owner sa)
sind durch den Aufruf von Invoke-sqmDatabaseStandardization ersetzt. Neu nach jedem Restore:
Kompatibilitaetsstufe auf Serverniveau und TARGET_RECOVERY_TIME 60 s. Mit -KeepCompatibilityLevel
bleibt die Stufe aus dem Backup erhalten (Anwendung noch nicht fuer die neue Stufe freigegeben).
Behobene Schwaechen des alten Schritts 8:
- DROP USER lief mit -ErrorAction SilentlyContinue. Besass der User ein Schema, scheiterte der
Drop, gemeldet wurde trotzdem "Success". Jetzt: Schema/Rolle an dbo, Fehler werden als Failed gemeldet.
- Es wurden nur Windows-User/-Gruppen betrachtet, SQL-User ohne Login blieben liegen.
- Windows-User mit Zugriff ueber eine Gruppe wurden geloescht.
Die Aktionsnamen im Ergebnis haben sich geaendert: FixOrphans/RemoveOrphanWindowsLogins/SetDbOwner
heissen jetzt FixOrphanUser/RemoveUserWithoutLogin/DatabaseOwner, dazu CompatibilityLevel und
TargetRecoveryTime (eine Zeile pro User bzw. Einstellung).
Der dokumentierte Aufruf Invoke-sqmRestoreDatabase -DatabaseName X (juengste Backupkette aus der
Historie) scheiterte beim Parameter-Binding mit "fehlender Pflichtparameter BackupFile": mit nur
-DatabaseName passen alle drei Parametersaetze, PowerShell nahm den Default SingleFile.
Default ist jetzt FromHistory.
Live gegen DEV01 (SQL 2022) unter PS 5.1 und PS 7 geprueft: alle fuenf Schritte, -WhatIf ohne
Aenderung, zweiter Lauf nur Skipped, Gruppenzugriffs-Schutz (temporaerer Login
"Authentifizierte Benutzer"), Namen mit ] und Hochkomma, Restore mit und ohne
-KeepCompatibilityLevel. 20 neue Unit-Tests, gesamte Suite gruen (545 passed).
Ein Zeitplan "Weekly, Sunday" erschien im HTML-Report nur als Weekly, die eigentliche Information
fehlte. Ursache: freq_interval (bei Weekly eine Bitmaske der Wochentage) wurde nicht ausgewertet,
freq_recurrence_factor und freq_relative_interval wurden gar nicht erst abgefragt.
Jetzt vollstaendig:
| Zeitplan | vorher | jetzt |
|---|---|---|
| Weekly, Sonntag 02:30 | Weekly @ 02:30 |
Weekly on Sunday @ 02:30 |
| alle 2 Wochen Mo+Fr, 06-18 Uhr alle 2 h | Weekly every 2 hour(s) @ 06:00 |
Weekly (every 2 weeks) on Monday, Friday every 2 hour(s) between 06:00 and 18:00 |
| Monatlich letzter Sonntag | Monthly (relative) |
Monthly on the last Sunday @ 05:00 |
| Monatlich Tag 15, alle 3 Monate | Monthly (day 15) |
Monthly (every 3 months) on day 15 @ 04:00 |
| Einmalig | One Time |
One Time on 2026-10-01 @ 12:00 |
Ausserdem: Startzeit 00:00 wurde bisher unterschlagen (Bedingung -gt 0), bei Intervall-Zeitplaenen
wird das Zeitfenster (active_end_time) genannt, sofern es nicht der ganze Tag ist.
Live gegen DEV01 unter PS 5.1 verifiziert, 10 neue Unit-Tests fuer den Zeitplantext.
Bisher gab es dafuer keine Funktion. Der naheliegende Weg ueber dbatools scheidet fuer den
haeufigsten Fall aus: Move-DbaDbFile lehnt master, model, msdb und tempdb grundsaetzlich ab
("System database detected as input"), und gerade tempdb ist die Datenbank, die man verschieben
will, wenn ein Laufwerk zu klein wird.
Drei Ablaeufe, je nach Datenbank:
| Datenbank | Ablauf | Dateien kopieren |
|---|---|---|
| Benutzerdatenbank | OFFLINE -> kopieren -> MODIFY FILE -> ONLINE | ja |
| tempdb | MODIFY FILE -> Instanz-Neustart | nein, wird beim Start neu angelegt |
| model / msdb | MODIFY FILE -> Dienst stoppen -> kopieren -> Dienst starten | ja |
| master | abgelehnt, dafuer sind die Startparameter -d/-l/-e zustaendig | - |
Bei tempdb bleiben die alten Dateien sonst als Leichen auf dem alten Laufwerk liegen, weil SQL Server sie beim Start nicht aufraeumt, sondern am neuen Ort neue anlegt. Die Funktion entfernt sie nach dem Neustart, aber erst nachdem der neue Pfad per Test-DbaPath bestaetigt ist.
Vorgeprueft wird, bevor irgendetwas veraendert wird:
Get-sqmDatabaseAgMembership (eine AG-Datenbank laesst sich nicht offline setzen).CREATE FILE encountered operating system error 5(Access is denied.).Get-DbaDiskSpace (kennt Mountpoints), Rueckfallebene xp_fixeddrives.Scheitert eine Vorpruefung, wird nichts veraendert. Scheitert das Kopieren mitten im Vorgang,
werden bereits abgesetzte MODIFY FILE zurueckgesetzt und die Datenbank geht mit den alten Pfaden
wieder online. Der SQL Server Agent wird nach einer Dienstaktion in den Zustand von vorher
zurueckversetzt - Start-DbaService -Type Engine startet ihn nicht mit, und ein stillschweigend
gestoppter Agent faellt sonst erst auf, wenn nachts die Sicherung ausbleibt.
-WhatIf laeuft alle Vorpruefungen durch und zeigt den geplanten Ablauf, ohne etwas zu aendern.
Beim Test aufgefallen und in dieser Funktion vermieden: Join-Path loest den Laufwerksbuchstaben
gegen die PSDrives der aufrufenden Maschine auf. Fuer ein G:\... auf einem entfernten SQL
Server, das es lokal nicht gibt, liefert es einen nicht-terminierenden Fehler und einen LEEREN
String:
PS> Join-Path 'G:\TempDB' 'tempdb.mdf'
Join-Path: Laufwerk wurde nicht gefunden. Ein Laufwerk mit dem Namen "G" ist nicht vorhanden.
PS> "'$r'"
''
Das haette hier zu ALTER DATABASE ... FILENAME = N'' gefuehrt. Die Funktion setzt Serverpfade
darum ueber einen eigenen String-Join zusammen (_CombineServerPath). Split-Path -Parent/-Leaf
ist davon nicht betroffen und funktioniert auch fuer fremde Laufwerke.
Die Backup-Abfrage filterte COPY_ONLY-Sicherungen mit is_copy_only = 0 komplett weg, so dass
LastFullBackup als (keins) im Bericht stand, waehrend die SSMS-Datenbankeigenschaften ein
Datum anzeigten. Im Container nachgestellt und gemessen (SQL 2022, frische DB ohne Historie):
| Fall | msdb.dbo.backupset | SMO/SSMS LastBackupDate | alte Health-Abfrage |
|---|---|---|---|
| normales FULL | type=D, copy_only=False |
Datum | Datum |
| COPY_ONLY FULL | type=D, copy_only=True |
Datum | keine Zeile -> (keins) |
| nur FILEGROUP | type=F |
leer | keine Zeile |
| DB nach Backup umbenannt | Name passt nicht mehr | leer | keine Zeile |
Nur der COPY_ONLY-Fall erzeugt also den Widerspruch, weil SMO nicht auf is_copy_only filtert.
COPY_ONLY zaehlt weiterhin nicht als regulaeres Full, denn es setzt die Differential-Basis nicht zurueck und bildet keine Wiederherstellungskette. Neu ist nur, dass es benannt statt verschwiegen wird:
LastFullBackup zeigt (nur COPY_ONLY: 2026-09-18 06:18), wenn es ausschliesslich COPY_ONLY
gibt, sonst unveraendert das Datum des regulaeren Fulls bzw. (keins).LastCopyOnlyFull in Objekt, CSV und HTML (Spalte "Nur COPY_ONLY").Die Spalte SizeMB wird als {0:N1} der aktuellen Kultur formatiert (z. B. 12.345,6) und ueber
die neue CSS-Klasse .num in ConvertTo-sqmHtmlReport rechtsbuendig mit tabular-nums
ausgegeben. CSV und TXT liefern weiterhin den unformatierten Zahlwert, damit sie auswertbar
bleiben.
Bisher gab es zwei Einheiten fuer dieselbe Sache: auf PowerShell-Seite ein String mit Suffix
('48h', '7d', '4w', '1m'), im Job-Step und in Olas @CleanupTime dagegen eine Zahl in Stunden.
Die Funktion rechnete dazwischen um, was beim Lesen des Job-Steps unnoetig Fragen aufwirft
(@CleanupTime = 672 - was bedeutet 672?).
-CleanupTime ist jetzt in beiden Funktionen ein [int] in Stunden, also dieselbe Einheit, die
Ola selbst verwendet, und der Wert wird unveraendert durchgereicht:
New-sqmBackupMaintenanceJob -CleanupTime 672 -> @CleanupTime = 672 im Job-Step.
Typ-Defaults jetzt als Stunden: FULL 672 (4 Wochen), DIFF 336 (2 Wochen), LOG 48 (2 Tage).Invoke-sqmUserDatabaseBackup -CleanupTime 48 - intern wird daraus fuer
Remove-DbaBackup -RetentionPeriod wieder '48h' gebaut, weil dieses Cmdlet das Suffix-Format
verlangt. Nach aussen ist die Einheit aber einheitlich Stunden.0 bedeutet in beiden Funktionen "kein Cleanup", genau wie -NoCleanup.Die alte Schreibweise mit Suffix wird abgewiesen (-CleanupTime '48h' ist kein gueltiger int).
Im Container als echter Agent-Job geprueft: Defaults 672/336/48 landen korrekt im Job-Step,
-CleanupTime 24 ebenso, -NoCleanup erzeugt @CleanupTime = NULL (und wirft dabei nicht mehr
den MetadataError, den ein ValidateRange(1, ...) beim Setzen auf 0 ausgeloest hat - die Range
beginnt jetzt bei 0), und die Umrechnung int -> '48h' fuer Remove-DbaBackup loescht eine 10 Tage
alte .bak-Datei und laesst die frische stehen.
In 1.9.143.0 steckten Verzeichnis, Backup-Typ und Aufbewahrung fest verdrahtet im Prozedur-Rumpf, und es gab zwei Prozeduren (eine Sync-Prozedur plus eine Run-Prozedur JE JOB). Beides ist weg:
master.dbo.sqm_BackupUserDatabases fuer alle Jobs und
alle Backup-Typen. Die Sync-Logik der Exclude-Tabelle steckt jetzt in derselben Prozedur
(@SyncExcludeTable), eine zweite Prozedur je Job entfaellt ersatzlos.Der Job-Step zeigt alle Werte im Klartext und laesst sich direkt dort anpassen:
EXEC master.dbo.[sqm_BackupUserDatabases] @BackupType = N'FULL', @Directory = N'D:\Backup\Usr-db', @CleanupTime = 672, @UseExcludeTable = 1, ...
Da die Sync-Logik mit in die Prozedur gewandert ist, hat der Job jetzt nur noch EINEN Step statt zwei.
Im SQL-2022-Container mit aktiviertem Agent als echter Agent-Job geprueft: genau eine Prozedur in master, ein Step je Job mit sichtbaren Parametern (@CleanupTime 672 fuer FULL, 48 fuer LOG), FULL-Job 3 s und LOG-Job 0 s jeweils erfolgreich, eine per sqm_BackupExclude ausgeschlossene Datenbank blieb korrekt unangetastet, und Olas CommandLog belegt die Einzelaufrufe je Datenbank mit dem uebergebenen Verzeichnis.
Die 1.9.142.0-Umstellung auf T-SQL hat die komplette Logik als langen Skripttext in die Job-Steps geschrieben (Step 1 ~1.800, Step 2 ~4.000 Zeichen). Beides liegt jetzt als Prozedur in master, die Job-Steps rufen sie nur noch auf - gleiche Konvention wie in New-sqmOlaUsrDbBackupJob:
EXEC master.dbo.[sqm_SyncBackupExcludeTable] @IncludeSystemDatabases = 0; (73 Zeichen).
Eine gemeinsame Prozedur fuer alle Backup-Typen statt einer Kopie je Job, der System-DB-Filter
ist ein Parameter.EXEC master.dbo.[sqm_Run_<JobName>]; (~53 Zeichen). Enthaelt den Cursor, der pro
Datenbank Olas DatabaseBackup aufruft, je Datenbank in TRY/CATCH.Beide Prozeduren werden bei jedem Aufruf der Funktion neu angelegt (DROP + CREATE), ein
-Update-Lauf aktualisiert also auch die Backup-Logik selbst und nicht nur den Job.
Nebeneffekt der Prozedur-Form: innerhalb einer Prozedur greift Deferred Name Resolution, der Verweis auf master.dbo.sqm_BackupExclude darf also direkt stehen. Der sp_executesql-Umweg aus 1.9.141.0/1.9.142.0 (noetig, weil ein Ad-hoc-Batch alle Tabellennamen schon beim Parsen bindet) entfaellt damit in beiden Prozeduren.
Auf DEV01 als echter Agent-Job geprueft, jeweils mit vorher geloeschter sqm_BackupExclude, um das Anlegen der Prozedur ohne existierende Tabelle mitzutesten: FULL-Job 11 s, alle 19 Benutzerdatenbanken gesichert; LOG-Job 6 s, 15 Log-Sicherungen - also 19 minus 3 Datenbanken im Recovery Model SIMPLE minus die eine per sqm_BackupExclude ausgeschlossene.
Die eigentliche Ursache fuer "Job startet, kein Backup, kommt nie zurueck": Step 1 war ein
PowerShell-Step (Import-Module sqmSQLTool; Sync-sqmBackupExcludeTable). Der blieb im
PowerShell-Subsystem des SQL Agent haengen und kehrte nie zurueck - und weil Step 1 nie fertig
wurde, lief Step 2 (das Backup) ueberhaupt nie an. Genau das erklaert, warum gar kein Backup
entstand und der Job trotzdem ewig "Wird ausgefuehrt" zeigte. Derselbe Befehlstext von Hand in
einer PowerShell-Konsole ausgefuehrt lief immer sauber durch, weil sich dort der Konsolen-Prozess
selbst beendet. Die Fixes in 1.9.140.0 (exit 0, -Confirm $false) haben daran nichts geaendert.
Beide Steps laufen jetzt im Subsystem TransactSql, ganz ohne PowerShell:
master.dbo.sqm_BackupExclude bei Bedarf an, traegt neue Datenbanken ein
(IsActive=1), markiert verschwundene als IsOrphaned=1 und nimmt die Markierung wieder zurueck,
wenn eine Datenbank zurueckkehrt - alles als T-SQL.master.dbo.DatabaseBackup auf. Jeder Aufruf steckt in TRY/CATCH: eine fehlschlagende
Datenbank stoppt den Lauf nicht, der Step faellt erst am Ende aus, wenn mindestens eine
Datenbank gescheitert ist. Mit -UseExcludeTable wird jede Datenbank einzeln gegen
sqm_BackupExclude geprueft.Nebeneffekt: der Job braucht das sqmSQLTool-Modul auf dem SQL Server gar nicht mehr, weil in den
Steps nur noch T-SQL steht. Voraussetzung ist jetzt Olas Maintenance Solution auf der Instanz
(Install-sqmOlaMaintenanceSolution). -CleanupTime ('48h'/'7d'/'4w') wird in Olas
@CleanupTime (Stunden) umgerechnet, -CheckPreferredReplica auf Olas
@OverrideBackupPreference abgebildet, und -MailTo/-MailOnSuccess per sp_send_dbmail
direkt im T-SQL-Step umgesetzt.
Diesmal am echten Fehlerpfad getestet, nicht nur per Query: in einem SQL-2022-Container mit aktiviertem SQL Agent als echter Agent-Job gestartet (FULL und LOG, inkl. korrekt uebersprungener ausgeschlossener Datenbank), und anschliessend auf DEV01 - Job durchgelaufen in 11 Sekunden, beide Steps "Succeeded", alle 19 Benutzerdatenbanken gesichert.
Auch mit dem exit-0/-Confirm-Fix aus 1.9.140.0 blieb ein Backup-Maintenance-Job als SQL Agent Job weiterhin haengen (dauerhaft "Wird ausgefuehrt"), obwohl exakt derselbe Step- Befehlstext manuell in einer PowerShell-Konsole kopiert und ausgefuehrt anstandslos durchlief. Statt die PowerShell-Subsystem-Ursache weiter einzukreisen, ersetzt dieses neue Skript den Ansatz vollstaendig: keine PowerShell-Subsystem-Abhaengigkeit mehr, reines Transact-SQL fuer einen SQL Agent Job-Step vom Typ "Transact-SQL Script (T-SQL)".
Das Skript cursort durch die Kandidaten-Datenbanken (@Databases: ALL_DATABASES /
USER_DATABASES / explizite Liste), prueft optional master.dbo.sqm_BackupExclude pro
Datenbank (Tabelle nicht vorhanden = keine Ausschluesse) und ruft fuer jede nicht
ausgeschlossene Datenbank einzeln Ola Hallengrens master.dbo.DatabaseBackup auf. Jeder
Einzelaufruf steckt in TRY/CATCH: eine fehlschlagende Datenbank wird geloggt (RAISERROR)
und die Schleife laeuft weiter, der Job-Step schlaegt erst am Ende fehl, wenn ueberhaupt
mindestens eine Datenbank gescheitert ist - damit die Agent-History korrekt "N Datenbanken
fehlgeschlagen" zeigt, waehrend alle anderen trotzdem gesichert wurden.
Live gegen einen frischen SQL-2022-Testcontainer verifiziert: FULL-Lauf ueber drei Datenbanken erfolgreich, LOG-Lauf mit sqm_BackupExclude-Ausschluss einer Datenbank korrekt uebersprungen, und eine gezielt injizierte Einzeldatenbank-Fehlschlag-Simulation bestaetigt, dass die beiden anderen Datenbanken trotzdem fertig gesichert wurden, bevor der Job-Step als Ganzes fehlschlaegt.
Ein anfaenglicher Fehler beim Testen wurde dabei selbst gefunden und behoben: ein direkter Verweis auf master.dbo.sqm_BackupExclude in einem Ad-hoc-Batch (kein Stored- Procedure-Body) scheitert mit "Invalid object name", sobald die Tabelle nicht existiert - und zwar unabhaengig davon, ob der referenzierende Codepfad zur Laufzeit ueberhaupt erreicht wird, weil Ad-hoc-Batches (anders als Stored Procedures) alle Objektnamen beim Parsen binden. Der Tabellenzugriff laeuft deshalb ueber sp_executesql, das erst beim tatsaechlichen Ausfuehren aufgeloest wird.
Beide Job-Steps liefen als reiner Befehlstext im PowerShell-Subsystem des SQL Agent, ohne
abschliessendes exit. Sobald Step 2 dank der 1.9.137.0-Korrektur tatsaechlich erfolgreich
durchlief (vorher schlug er sofort mit dem ParameterBindingException-Fehler fehl und der
Prozess beendete sich dadurch schnell von selbst), blieb der Job-Prozess danach manchmal
haengen, obwohl das eigentliche Skript laengst fertig war - vermutlich durch SMO-
Connection-Pooling aus Connect-DbaInstance, das den Prozess am Leben haelt. Der Job zeigte
dauerhaft "Wird ausgefuehrt" in der Job-History, ohne je abzuschliessen. Manuelles Ausfuehren
desselben Befehlstexts in einer normalen PowerShell-Konsole zeigte den Effekt nicht, weil sich
dort der Konsolen-Prozess selbst regulaer beendet.
Beide generierten Step-Kommandos enden jetzt explizit mit exit 0 und setzen -Confirm $false
fuer die aufgerufene Funktion - genau das Muster, das New-sqmAgentCommandJob (generic-invoke.ps1)
fuer denselben PowerShell-Subsystem-Effekt bereits verwendet.
Weder Invoke-sqmUserDatabaseBackup noch New-sqmBackupMaintenanceJob hatten bisher eine
Retention-/Aufraeum-Funktion. Besonders fuer LOG-Jobs (jetzt standardmaessig alle 15 Minuten,
siehe 1.9.138.0) fuellt sich das Backup-Verzeichnis ohne Aufraeumung entsprechend schnell.
Invoke-sqmUserDatabaseBackup bekommt einen neuen -CleanupTime-Parameter (Format wie bei
dbatools Remove-DbaBackup: '48h', '7d', '4w', '1m'). Wenn gesetzt, werden nach dem Backup
alle Dateien MIT DERSELBEN Endung dieses Laufs (.bak fuer FULL/DIFF, .trn fuer LOG) im
BackupPath entfernt, die aelter als die angegebene Periode sind - ueber Remove-DbaBackup.
Ohne -CleanupTime bleibt das Verhalten wie bisher (keine Aufraeumung).
New-sqmBackupMaintenanceJob reicht -CleanupTime an Step 2 durch und setzt bei fehlender
Angabe automatisch einen typabhaengigen Default: FULL 4 Wochen, DIFF 2 Wochen, LOG 48 Stunden.
-NoCleanup deaktiviert die Aufraeumung fuer den erzeugten Job vollstaendig.
Live gegen DEV01 verifiziert: eine 10 Tage alte .trn-Datei wurde bei -CleanupTime "48h"
entfernt, eine gleich alte .bak-Datei im selben Verzeichnis blieb unangetastet (nur die
Endung des jeweiligen BackupType wird angefasst).
FULL laeuft jetzt standardmaessig taeglich um 20:15 statt nur sonntags um 20:00. LOG laeuft standardmaessig alle 15 Minuten rund um die Uhr (Start 00:00) statt einmalig um 20:00 - vorher haette der reine Default fuer -BackupType LOG (ScheduleIntervalMinutes=0) ueberhaupt keine wiederkehrende Sicherung erzeugt. DIFF bleibt unveraendert (Montag-Samstag, 20:00).
Alle drei Defaults gelten weiterhin nur, wenn -ScheduleDays/-ScheduleTime/ -ScheduleIntervalMinutes nicht explizit angegeben werden.
New-sqmBackupMaintenanceJob -BackupType LOG legte einen Job-Step namens
Backup-UserDatabases-LOG an, dessen tatsaechlicher Befehl aber immer eine FULL-Sicherung
ausloeste: Invoke-sqmUserDatabaseBackup kannte gar keinen -BackupType-Parameter und
sicherte intern hart verdrahtet mit Type = 'Full'. Ein mit -ScheduleIntervalMinutes 15
eingerichteter "LOG"-Job haette also alle 15 Minuten eine volle Datenbanksicherung
ausgefuehrt statt einer Transaktionsprotokollsicherung.
Invoke-sqmUserDatabaseBackup hat jetzt einen echten -BackupType-Parameter
(FULL/DIFF/LOG, Default FULL), der auf Backup-DbaDatabase -Type
(Full/Differential/Log) abgebildet wird. LOG-Sicherungen werden mit der Endung
.trn geschrieben (FULL/DIFF weiterhin .bak), passend zur ueberall sonst im Modul
verwendeten Konvention. New-sqmBackupMaintenanceJob reicht den gewaehlten -BackupType
jetzt tatsaechlich durch, statt ihn im Step-2-Kommando auf 'FULL' zu ueberschreiben.
Live gegen DEV01 verifiziert: -BackupType FULL erzeugt eine .bak-Datei (Backupset-Typ
D), -BackupType LOG eine .trn-Datei (Backupset-Typ L).
Waehrend sqlservr.exe -m ... im Single-User-Modus lief, ermittelte die Bereitschaftserkennung
den ERRORLOG-Pfad ueber eine eigene SQL-Verbindung (Invoke-DbaQuery/xp_readerrorlog). Diese
Verbindung konkurrierte mit jedem anderen Client, der sich zufaellig genau in diesem Moment mit
der Instanz verbinden wollte - Single-User-Modus laesst nur eine einzige Verbindung zu. Gewann
der fremde Client (im Feld beobachtet: NLB-PROD\izeyl24, abgewiesen mit "Server is in
single-user mode"), blieb $errorlogPath dauerhaft $null, die Bereitschaftserkennung lief nie
an und die Funktion wartete bis zum -StartupTimeoutSeconds-Timeout und killte den Prozess -
obwohl der Collation-Rebuild selbst laengst durchgelaufen sein konnte.
Der ERRORLOG-Pfad wird jetzt bereits im Pre-Flight-Check ermittelt, waehrend die Instanz noch
normal laeuft (Registry-Startparameter -e, mit Get-DbaErrorLogConfig als Fallback). Die
Warteschleife braucht danach keine SQL-Verbindung mehr und ist damit gegen fremde
Verbindungsversuche waehrend des Single-User-Fensters immun.
Werkzeuge wie ServiceNow Discovery, SCOM oder ein authentifizierter Schwachstellenscanner verbinden sich mit einem eigenen Dienstkonto. Fehlt dieses Konto auf einer Instanz, scheitert die Erfassung aus Sicht des DBA lautlos. Bei ServiceNow ist der Schaden dauerhaft: ein CI, das einmal als "absent" markiert wurde, kehrt nicht von selbst in den aktiven Status zurueck, auch wenn der naechste Scan wieder laeuft. Bisher fiel so etwas erst auf, wenn jemand die CMDB anzweifelte.
Test-sqmDiscoveryAccess prueft das ueber eine ganze Serverliste hinweg, rein lesend und ohne
jede Aenderung an den geprueften Instanzen. Pro Instanz wird gemeldet, ob das Login existiert,
aktiviert ist und nicht per DENY von CONNECT SQL ausgesperrt wurde, ob es Mitglied der
erwarteten Serverrolle ist, und welche effektiven Serverberechtigungen es tatsaechlich besitzt.
Die effektive Berechtigung wird nicht geraten, sondern aus drei Quellen zusammengesetzt:
direkte GRANTs, jede Serverrolle in der das Login steckt (rekursiv aufgeloest, Rollen koennen
verschachtelt sein) und die Rolle public. DENY schlaegt dabei GRANT, deshalb wird DENY
separat geprueft statt nur die Existenz eines GRANT.
Daraus leitet die Funktion eine DiscoveryLevel-Stufe ab, also das, was das sammelnde Werkzeug
wirklich sieht: None, ServerOnly, Databases, DatabaseDetails oder Full. Ueber
-RequiredLevel wird die Sollstufe vorgegeben, alles darunter gilt als Fehler.
Zwei Faelle, die in der Praxis besonders schwer zu finden sind, werden ausdruecklich erkannt:
VIEW ANY DATABASE ist im SQL Server standardmaessig an public vergeben. Genau deshalb
reicht ein blosses Login, um alle Datenbanken aufzulisten. Wurde das Recht im Rahmen einer
Haertung von public entzogen oder dem Login per DENY gesperrt, sieht das Konto nur noch
master und tempdb, und zwar ohne jede Fehlermeldung. Die Funktion unterscheidet beide
Ursachen.VIEW ANY DEFINITION liefert sys.master_files null Zeilen statt eines Fehlers.
Datenbankgroessen und Dateipfade bleiben leer, die Erfassung meldet trotzdem Erfolg.Ebenfalls gemeldet wird der umgekehrte Fall: ein Discovery-Konto mit sysadmin ist
ueberberechtigt, fuer keine der Sammelabfragen erforderlich und gehoert reduziert.
Ab SQL Server 2022 meldet der Server fehlende DMV-Rechte als VIEW SERVER PERFORMANCE STATE
statt VIEW SERVER STATE. Beide Schreibweisen werden ausgewertet, und der Hinweistext nennt
die Variante passend zur Version der geprueften Instanz.
Ausgabe als TXT- und HTML-Bericht unter <Get-sqmDefaultOutputPath>\DiscoveryAccess, zusaetzlich
als Objekte fuer die Weiterverarbeitung. -SqlInstance ist pipelinefaehig, eine Serverliste aus
einer Datei laesst sich also direkt durchreichen.
Abgrenzung: Bezieht ein Konto seinen Zugriff ueber ein AD-Gruppen-Login statt ueber ein eigenes
Login, meldet diese Funktion es als fehlend. Die Aufloesung verschachtelter AD-Gruppen ist
Aufgabe von Test-sqmLoginGroupAccess und setzt einen erreichbaren Domaenencontroller voraus.
Live gegen DEV01 geprueft, mit sieben Szenarien: fehlendes Login, nur Login, fehlendes VIEW ANY DEFINITION, vollstaendige Rolle, falsche Rollenmitgliedschaft, sysadmin-Ueberberechtigung und DENY VIEW ANY DATABASE. Zusaetzlich der Mehrinstanzlauf ueber die Pipeline mit einer nicht erreichbaren Instanz.
Test-sqmLoginGroupAccess brach am Ende jedes Laufs mit einer CommandNotFoundException
fuer if ab, und zwar erst beim Schreiben des TXT-Berichts, also nachdem die gesamte
AD- und SQL-Pruefung bereits gelaufen war. Ursache: in der -f-Formatzeile standen drei
Bedingungen als (if ... ) statt $(if ... ). Runde Klammern eroeffnen in PowerShell einen
Pipeline-Kontext, in dem if kein Schluesselwort mehr ist, sondern als Kommandoname
interpretiert wird. Das faellt beim Parsen nicht auf, sondern erst, wenn die Zeile ausgefuehrt
wird.
Dasselbe Muster steckte in Get-sqmCertificateReport als return if (...) { } else { }:
auch return nimmt eine Pipeline entgegen, also galt dort dieselbe Falle. Getroffen haette
es jeden Lauf, bei dem ein Endpoint-Zertifikat klassifiziert werden musste. Ersetzt durch
if (...) { return ... } plus abschliessendem return.
Zur Absicherung wurden alle Dateien unter Public\ und Private\ per AST daraufhin
durchsucht, ob irgendwo ein Schluesselwort (if, else, while, switch, try, ...) in
Kommandoposition steht. Nach den beiden Korrekturen: keine weiteren Treffer.
Add-sqmDatabaseToAG nimmt jetzt auch TDE-verschluesselte DatenbankenEine verschluesselte Datenbank ueber Automatic Seeding in eine Availability Group zu bringen
scheiterte bisher an zwei Voraussetzungen, die die Funktion nicht kannte: Seeding
verschluesselter Datenbanken gibt es erst ab SQL Server 2019, und das Zertifikat, an dem der
Datenbank-Verschluesselungsschluessel haengt, muss auf jedem Secondary bereits liegen. Fehlt
eines von beidem, bricht Add-DbaAgDatabase ab, nachdem die Funktion die Datenbank auf den
Secondaries schon geloescht hat.
Beides wird jetzt vorab geprueft, und zwar bevor irgendetwas veraendert wird. Der TDE-Zustand
aller Benutzerdatenbanken wird einmal aus sys.dm_database_encryption_keys gelesen; nur fuer
eine verschluesselte Datenbank laufen die weiteren Pruefungen. Ist ein Replikat aelter als SQL
2019, wird die Datenbank mit Status TdeUnsupportedVersion und Nennung des Knotens
uebersprungen, statt es zu versuchen. Fuer alles Unverschluesselte bleibt der Ablauf
unveraendert, inklusive der Zertifikatsabfragen, die dann gar nicht erst stattfinden.
Neu ist -SyncTdeCertificate: das Zertifikat wird vom Primary exportiert
(BACKUP CERTIFICATE ... WITH PRIVATE KEY) und auf jedem Secondary erzeugt, dem es fehlt.
Ohne den Schalter meldet die Funktion TdeCertificateMissing und nennt die betroffenen Knoten.
Dazu kommen -TdeCertificateBackupPath, -TdeCertificatePassword, -TdeMasterKeyPassword
und -KeepTdeCertificateBackup.
Vier Entscheidungen, die den Unterschied zu einem Skript machen:
TdeCertificateNameConflict
gemeldet.CREATE CERTIFICATE ... WITH PRIVATE KEY verlangt ihn in master. Fehlt er, wird er angelegt,
sonst scheitert der Import mit einer Meldung, die nach einem Kennwortfehler aussieht.TdeEncryptorNotCertificate gemeldet und nicht als fehlendes Zertifikat. Der Schluessel
laesst sich nicht als Datei verteilen, dafuer ist der Provider zustaendig..cer und .pvk nach der Verteilung entfernt, weil die .pvk den
privaten Schluessel traegt.Geprueft: 26 Unit-Tests mit gemockter AG sowie ein Lauf gegen einen echten SQL Server 2022 unter
PowerShell 5.1. Dort wurde die Anweisungsfolge einzeln nachgestellt, an einer TDE-Datenbank mit
einem Zertifikat, dessen Name eine eckige Klammer und dessen Kennwort ein Dollarzeichen und ein
Hochkomma enthaelt: Erkennungsabfrage, Export, Import in eine Datenbank ohne Hauptschluessel
(scheitert erwartungsgemaess), Anlegen des Hauptschluessels, erneuter Import, Abgleich der
Thumbprints. Der Livelauf brachte zwei Befunde, die jetzt im Code stehen: SQL Server lehnt ein
Zertifikat ab, dessen Thumbprint in der Datenbank schon existiert, auch unter anderem Namen -
deshalb wird nur dort erzeugt, wo der Thumbprint wirklich fehlt. Und NULL aus dem LEFT JOIN
kommt als [DBNull] an, nicht als $null, was die Pruefung auf den EKM-Fall stillschweigend
umgangen haette.
Nicht im Labor pruefbar und entsprechend gekennzeichnet: der Seeding-Vorgang selbst. In dieser Umgebung gibt es keine Availability Group; die AG-Pfade sind ueber Unit-Tests mit gemockten dbatools-Aufrufen abgedeckt.
Invoke-sqmSsisCatalogMigration - SSIS-Katalog auf einen neuen Server umziehen und anhebenEinen SSIS-Katalog umzuziehen ist kein Datenbankumzug. Die SSISDB traegt einen Datenbank-Hauptschluessel, und jeder empfindliche Wert darin - Kennwoerter in Verbindungsmanagern, als sensibel markierte Umgebungsvariablen, Projektparameter - haengt an diesem Schluessel. Ein blosses Backup/Restore setzt eine Datenbank auf den neuen Server, deren Hauptschluessel der neue Server nicht oeffnen kann; das faellt nicht beim Restore auf, sondern erst beim Ausfuehren der Pakete. Dazu kommt: der Restore schaltet TRUSTWORTHY ab, macht den wiederherstellenden Login zum Eigentuemer und laesst den Katalog auf der Schemaversion des ALTEN Servers stehen.
Die Funktion faehrt die vollstaendige Abfolge in einem Lauf und protokolliert jeden Schritt:
Bewertung von Quelle und Ziel (Inventar, Schemaversion, Schluesselzustand, TRUSTWORTHY,
Eigentuemer, CLR, Wartungsjob, sp_ssis_startup), Sicherung von Hauptschluessel und Datenbank,
Anlegen des Katalogs auf dem Ziel, Restore, Nacharbeit (TRUSTWORTHY, Eigentuemer, Umschluesseln
auf den Service Master Key des Ziels, catalog.startup, verwaiste Benutzer) und schliesslich der
Schemavergleich. Die Quelle wird dabei nie veraendert; sie bleibt online, es laufen nur Lesezugriffe
und zwei Sicherungen.
Drei Entscheidungen, die den Unterschied zu einem Skript machen:
-AssessOnly als eigener Einstieg. Erst planen, dann umziehen. Der Bewertungslauf meldet
jeden Blocker (keine SSISDB, aelteres Ziel, vorhandene SSISDB ohne -Force, unlesbare
Katalogsichten) und aendert nichts.-UpgradeCatalog stoesst
ISDBUpgradeWizard.exe an und prueft anschliessend die Schemaversion nach: ein wirkungsloser
Lauf wird als Fehler gemeldet, nicht als Erfolg.Der Restore selbst geht an Invoke-sqmRestoreDatabase - damit gilt auch hier die erprobte
Behandlung von Exclusive Access (SINGLE_USER, Fremdsessions beenden, Wiederholung), und es gibt
weiterhin nur eine Stelle im Modul, die RESTORE DATABASE ausfuehrt. Der Scan nach Agent-Jobs, die
nach dem Umzug umgehaengt werden muessen, nutzt Find-sqmAgentJobReference.
Geprueft: 53 Unit-Tests plus Laeufe gegen einen echten SQL Server 2022 unter PowerShell 5.1. Die
Anweisungsfolge zum Umschluesseln (BACKUP MASTER KEY, OPEN + ALTER MASTER KEY ADD ENCRYPTION
BY SERVICE MASTER KEY, RESTORE MASTER KEY) wurde dort einzeln nachgestellt; dabei kam heraus,
dass RESTORE MASTER KEY allein NICHT genuegt (is_master_key_encrypted_by_server bleibt danach
0) und ueber einen vorhandenen Schluessel nur mit FORCE laeuft - beides steckt jetzt in der
Funktion. Ein zweiter Befund kam aus dem Livelauf: die Pruefung des Hauptschluessels hing an
derselben Abfrage wie die Katalogsichten, sodass bei beschaedigtem Katalog ausgerechnet die
Kennwortpruefung stillschweigend uebersprungen wurde. Beide Abfragen sind jetzt getrennt, mit
Regressionstest.
Nicht im Labor pruefbar und entsprechend gekennzeichnet: das Anlegen des Katalogs, der Restore einer echten SSISDB und der Aufruf des Upgrade-Assistenten - auf keinem Rechner dieser Umgebung ist Integration Services installiert.
Find-sqmAgentJobReference - welcher Agent-Job ruft diese Prozedur auf?Die Frage kommt vor jedem Aufraeumen: Eine Prozedur soll geloescht oder umbenannt werden, eine Datenbank soll weg, oder eine Tabelle wird nachts leer und keine Anwendung will es gewesen sein. Bisher hiess die Antwort SSMS oeffnen und die Jobs durchklicken.
Find-sqmAgentJobReference liest alle Jobsteps aus msdb.dbo.sysjobsteps und meldet je
gefundenen Step eine Zeile: Job, Step, Subsystem, Zeitplan, letzter Lauf, Fundzeile mit
Zeilennummer. Gesucht wird nach Objektname (-ObjectName, mit Wildcards und optional schema-
oder datenbankqualifiziert), nach freiem Text (-SearchText, auf Wunsch als regulaerer
Ausdruck) und/oder nach Datenbank (-Database) - -Database allein beantwortet die Frage vor
dem Abschalten einer Datenbank.
Bewusst KEINE serverseitige command LIKE '%name%'-Suche: LIKE behandelt _ als Platzhalter,
und Prozedurnamen mit Unterstrich sind der Normalfall - usp_LoadSales wuerde auch
uspXLoadYSales finden. LIKE kennt ausserdem keine Wortgrenze und wuerde usp_LoadSalesArchive
mitliefern. Der Abgleich laeuft daher in PowerShell ueber Identifier-Grenzen, die auch die
Klammerschreibweise [dbo].[usp_LoadSales] erfassen.
Ein Textfund allein ist kein Beweis fuer einen Aufruf, deshalb wird jeder Fund eingeordnet:
CallType 'Execute' (der Name steht hinter EXEC/EXECUTE), 'Reference' (nur erwaehnt, etwa in
dynamischem SQL oder als Tabellenname) oder 'Text' (Treffer aus -SearchText), dazu
InComment, wenn die Fundstelle in einem Kommentar steht. Die Datenbank des Treffers wird aus
database_name des Steps, aus USE <db>, aus dem Datenbankteil dreiteiliger Namen sowie aus
sqlcmd -d und -Database von CmdExec-/PowerShell-Steps aufgeloest. -VerifyObject prueft
zusaetzlich in sys.objects, ob das aufgerufene Objekt dort ueberhaupt (noch) existiert - damit
wird aus "der Job nennt diesen Namen" ein "der Job ruft eine Prozedur auf, die es nicht mehr
gibt".
Live gegen einen echten SQL Server geprueft. Dabei ist ein Fehler aufgefallen und behoben
worden: msdb speichert einen nie gelaufenen Jobstep mit last_run_outcome = 0 und
last_run_date = 0, und 0 ist zugleich der Code fuer 'Failed' - acht Ola-Jobs ohne Historie
wurden dadurch zunaechst als fehlgeschlagen gemeldet. Massgeblich ist jetzt der Zeitstempel;
ohne Lauf steht 'NeverRun' im Ergebnis.
50 Unit-Tests decken Wortgrenze, Unterstrich-Falle, Klammerschreibweise, Kommentar- und
Erwaehnungserkennung, Datenbankaufloesung, Filter und die Objektpruefung ab. In
category-map.ps1 und nlp-synonyms.ps1 registriert.
Invoke-sqmPreferredPrimaryCheck + New-sqmPreferredPrimaryJob - AG kehrt selbst auf den gewuenschten Primary-Knoten zurueckNach einem Patchwochenende liegt die AG auf dem Knoten, der zufaellig zuletzt rebootet
wurde. Wo ein bestimmter Knoten Primary sein muss, war das bisher Handarbeit:
Get-sqmAlwaysOnHealthReport schauen, dann Invoke-sqmFailover von Hand.
Invoke-sqmPreferredPrimaryCheck vergleicht das aktuelle Primaerreplikat mit dem
vorgegebenen und schwenkt zurueck, wenn das gefahrlos moeglich ist. Es schwenkt NUR, wenn
jede Bedingung erfuellt ist: Ziel ist SECONDARY im SYNCHRONOUS_COMMIT-Modus, alle
AG-Datenbanken sind dort gejoint (Abgleich gegen sys.availability_databases_cluster -
faengt die nach dem Patchen nicht wieder gejointe Datenbank ab), SYNCHRONIZED, ONLINE und
nicht suspendiert, Redo-Queue unterhalb der Grenze, SQL Server auf dem Ziel laeuft lange
genug (ein Knoten kurz nach dem Reboot patcht evtl. noch weiter) und das aktuelle
Primaerreplikat haelt seine Rolle lange genug (Flatterschutz gegen das Hineinschwenken in
eine laufende Patchsequenz). Optional zusaetzlich ein Zeitfenster ueber -AllowedDay und
-AllowedTimeStart/-AllowedTimeEnd, z. B. "nur Mo-Fr 06:00-20:00" - damit bleibt das
Patchwochenende selbst unberuehrt.
Greift eine Bedingung nicht, passiert nichts; das Ergebnisobjekt fuehrt jede Pruefung
einzeln mit Begruendung auf. Der Schwenk selbst laeuft ueber Invoke-sqmFailover, es gibt
also weiterhin nur eine Stelle mit ALTER AVAILABILITY GROUP ... FAILOVER und dieselben
Pre-/Post-Checks wie beim Schwenken von Hand. Ein FORCED Failover wird nie automatisiert:
ist das Primaerreplikat nicht erreichbar, passiert nichts.
New-sqmPreferredPrimaryJob legt den passenden Agent-Job an - technisch
New-sqmAgentCommandJob (generischer CmdExec-Wrapper mit typisierten Clixml-Parametern)
plus den Minutenzeitplan, den dieser nicht anbietet (Daily + SubdayType Minutes). Fuer die
Einfuehrung -CheckOnly: der Job meldet dann nur, was er tun wuerde. -FailOnBlocked laesst
den Jobstep rot werden, wenn ein noetiger Schwenk blockiert wurde; ohne den Schalter bleibt
es bei WARNING im Log, damit ein Patchwochenende nicht jeden Lauf rot faerbt.
Beide Funktionen sind in category-map.ps1 und nlp-synonyms.ps1 registriert; 57 neue
Unit-Tests decken die Entscheidungslogik mit gemockten DMV-Antworten ab.
Show-sqmWhoIsActiveMonitor, live grid view for Get-sqmWhoIsActiveGet-sqmWhoIsActive's own repeat loop only prints a console table and writes
CSV/HTML at the end - not useful while actually watching a server live in the GUI.
Show-sqmWhoIsActiveMonitor is a WinForms dialog that shows the current snapshot in
a DataGridView and refreshes it automatically every N seconds ("Start"/"Stop", with
an adjustable interval and the same ShowSleepingSpids filter). Blocked sessions are
highlighted in red, sessions running/idle 30s+ in yellow - same severity classes as
the HTML report.
"Stop" (or closing the window mid-run) writes a report for that run exactly like
Get-sqmWhoIsActive does: the CSV contains every iteration, the HTML report only the
last snapshot - a long-running live session must not turn into a multi-thousand-row
HTML dump.
Refactored the query and report-writing logic that used to live inline in
Get-sqmWhoIsActive into two private helpers (Get-sqmWhoIsActiveSnapshot,
Export-sqmWhoIsActiveReport) so both the CLI/Agent-job function and this new GUI
monitor use the identical DMV query and HTML/CSV format - Get-sqmWhoIsActive's own
behavior/output is unchanged.
Get-sqmWaitStatistics, tooltips explaining what each wait type meansThe HTML report's WaitType column now carries a hover tooltip explaining what the wait
type actually is - independent of the existing threshold-based Recommendation column,
which only judges whether the current value is a problem. Backed by a new WaitDef_*
glossary in both language resource files, covering all 25 categorized wait types plus
7 common ones that had no category/recommendation entry (OLEDB, BACKUPBUFFER,
BACKUPIO, BACKUPTHREAD, MSQL_XP, BUFFERPOOL_SCAN,
WAIT_ON_SYNC_STATISTICS_REFRESH); any other wait type falls back to a generic
"see Microsoft/SQLskills docs" tooltip instead of showing nothing. The definition is
also returned as a new Definition property on the result objects (and therefore in
the CSV export). Switched the HTML body from ConvertTo-Html -Fragment to a hand-built
table, since a tooltip needs a title attribute ConvertTo-Html has no way to add.
Live-verified against DEV01: categorized types (CXSYNC_PORT, WRITELOG, ...) show
their specific explanation, uncategorized ones (PREEMPTIVE_OS_QUERYREGISTRY,
LOGBUFFER, ...) fall back cleanly, and the generated HTML properly encodes
apostrophes inside the title attribute.
Get-sqmWhoIsActive, sp_whoisactive-style live session monitor, repeatableShows currently active/blocked sessions (SPID, login/host/program, database, status,
blocking SPID, wait info, elapsed time, CPU/reads/writes, tempdb allocation, running
SQL statement) similar to Adam Machanic's sp_whoisactive, built entirely from DMVs
(sys.dm_exec_sessions/dm_exec_requests/dm_exec_sql_text/dm_db_session_space_usage)
so no stored procedure needs to be installed on the target instance.
-ShowSleepingSpids mirrors sp_whoisactive's @show_sleeping_spids (0 = active only,
1 = active + open transactions [default], 2 = all user sessions). New relative to
sp_whoisactive itself: -RepeatIntervalSeconds/-RepeatCount/-DurationMinutes turn
it into a refreshing live monitor (client-side loop, since the stored proc has no
built-in repeat), printing a table per snapshot and collecting every snapshot into one
CSV plus an HTML report of the last snapshot at the end - including when a long-running
loop is cancelled with Ctrl+C.
Repair-sqmSsasSerializeError, fixes the known SSAS "ASDatabase::Serialize" internal errorAddresses a documented SSAS bug (Microsoft Q&A 1350375): deploying an XMLA <Create>/<Alter>
script fails with Internal error: An unexpected error occurred (file 'pcserialize.cpp', line
1535, function 'ASDatabase::Serialize'), most commonly reported on instances that are part of
an Always On Availability Group. The confirmed fix is removing the undocumented
<Gen2ServerKey> element from msmdsrv.ini and restarting the SSAS service.
Uses the same instance/config-file discovery as Get-/Set-sqmSsasDeploymentMode (Windows
service command line -s switch, XML load with whitespace preserved to keep the diff minimal).
Idempotent - a second run reports NotPresent instead of re-touching the file. Backs up
msmdsrv.ini before writing, supports -WhatIf/-Confirm, and takes -RestartService to
apply the fix immediately instead of leaving it for the next manual restart.
Not live-tested against DEV01 - the lab has no SSAS instance installed. Verified only by
reading the XML structure Set-sqmSsasDeploymentMode already parses successfully in production.
Compare-sqmServerConfiguration meldet Verbindungsfehler, statt $null zu liefernScheiterte Connect-DbaInstance fuer eine der beiden Instanzen (z.B. Firewall blockiert den Port),
landete das nur im aeusseren catch: Eintrag in die Logdatei, return $null, nichts auf der Konsole
und kein Ergebnisobjekt, das der Aufrufer auswerten koennte - eine stille Sackgasse.
Quelle und Ziel werden jetzt einzeln verbunden und einzeln abgefangen, die Meldung nennt also die
nicht erreichbare Instanz. Dieser Pfad und der allgemeine catch schreiben jetzt Write-Error
(sichtbar auch ohne -EnableException) und liefern eine Ergebnisliste mit einer
Critical-Zeile Connection, die den Fehler beschreibt, statt $null.
-q machte den Collation-Rebuild zur stillen NulloperationEin realer Server lieferte nach zwei vorherigen Korrekturen (1.9.121.0 Readiness-Retry,
1.9.122.0 Stream-Drain) exakt denselben Fehler: gleiche Warnung "The system cannot find the file
specified", gleicher Abbruch bei der abschliessenden Pruefung. Beide Korrekturen betrafen das
Timing rund um sqlservr.exe, keine hat am Ergebnis etwas geaendert. Das hiess: der eigentliche
Rebuild lief gar nicht, unabhaengig vom Timing.
sqlservr.exe hat einen eigenen Argument-Parser, kein getopt: Schalter mit Wert (-d, -e,
-l und ebenso -q) verlangen den Wert ohne Leerzeichen. Der Code uebergab -q "$NewCollation"
mit Leerzeichen. Das wird sehr wahrscheinlich als leeres -q plus unbekanntes Zusatzargument
gelesen, sqlservr.exe startet dann normal im Single-User-Modus und baut nichts um.
Fix: -q"$NewCollation" ohne Leerzeichen, so wie sqlservr.exe es erwartet.
Set-sqmMaxMemory und Test-sqmMaxMemory berechneten den empfohlenen Wert als Prozentsatz des
gesamten Arbeitsspeichers, ohne andere Instanzen auf demselben Host zu kennen. Laufen dort
mehrere Engine-Instanzen, ueberbucht derselbe Prozentsatz pro Instanz den Speicher massiv (zwei
Instanzen mit je 90 % = 180 %), und Test-sqmMaxMemory meldete einen korrekt aufgeteilten Wert
als TooLow, weil auch das Toleranzband gegen den Gesamtspeicher gerechnet wurde.
Neu: Get-sqmHostEngineInstanceCount (Private) zaehlt die Engine-Instanzen eines Hosts ueber
Get-DbaService, mit Rueckfall auf Get-Service (MSSQLSERVER / MSSQL$<Name>), wenn der Host die
lokale Maschine ist und Get-DbaService nicht verfuegbar ist (z.B. WinRM gesperrt). Beide
oeffentlichen Funktionen teilen das Speicherbudget (und die Toleranzgrenzen) durch diese Anzahl.
Neuer Parameter -InstanceCount uebersteuert die automatische Erkennung.
Die ausfuehrliche Ausgabe eines realen Laufs zeigte xp_readerrorlog mit "The system cannot find
the file specified", dem klassischen Named-Pipes-Fehler 2: es hat nie jemand zugehoert. Dazu kam:
RedirectStandardOutput und RedirectStandardError waren fuer sqlservr.exe gesetzt, wurden
aber nirgends gelesen. Der .NET-Pipe-Puffer fasst nur wenige KB, sqlservr.exe mit
-T4022 -T3659 erzeugt beim -q-Rebuild weit mehr. Ist der Puffer voll, blockiert der Prozess
beim naechsten Schreibversuch und haengt bis zum Ablauf von -StartupTimeoutSeconds. Kill()
bricht den Rebuild dann mittendrin ab, der Dienst startet unauffaellig mit der alten
Collation, und erst die abschliessende Pruefung wirft.
Fix: beide Streams werden asynchron geleert (OutputDataReceived/ErrorDataReceived mit
BeginOutputReadLine/BeginErrorReadLine), die mitgeschnittene Ausgabe wird protokolliert und in
der Timeout-Warnung aus 1.9.121.0 angezeigt. Ein naechster Fehlschlag zeigt so die Diagnose von
sqlservr.exe selbst statt nichts.
-ExcludeDatabaseInvoke-sqmCollationChange suchte die Bereitschaftsmeldung im Errorlog von sqlservr.exe genau
einmal, direkt nach Process.Start(). Zu diesem Zeitpunkt nimmt die Instanz noch keine
Verbindungen an, die Pruefung scheiterte also fast immer still, und die Readiness-Erkennung griff
nie. Uebrig blieben das Selbstbeenden von sqlservr.exe oder ein blinder
StartupTimeoutSeconds-Timeout mit anschliessendem Kill(), der den Rebuild abbrechen kann,
bevor er festgeschrieben ist. Der Dienst startet dann normal mit der alten Collation, und ausser
dem abschliessenden Pruefungsfehler zeigt nichts das Problem an.
Fix: die Errorlog-Abfrage laeuft jetzt in jedem Poll-Durchlauf statt einmal, und die Funktion warnt ausdruecklich, wenn der Prozess beendet werden musste, ohne dass je eine Bereitschaftsmeldung kam, statt bedingungslos gruen "abgeschlossen" zu melden.
Ausserdem war -ExcludeDatabase dauerhaft wirkungslos: im inneren Where-Object ueberdeckte $_
das aeussere Datenbankobjekt mit dem gerade geprueften Muster-String, $_.Name war also immer
$null, und der Filter schloss nie etwas aus.
Der Click-Handler des "List..."-Knopfs fuer Invoke-sqmCollationChange prueft jetzt, ob
$script:guiState, .Controls und .Creds schon befuellt sind. Vorher warf er in
Show-sqmToolGui.ps1 Zeile 773 "Cannot index into a null array".
Test-sqmLoginGroupAccess - Zugriffsprobleme bei AD-Gruppen-Logins diagnostizierenIn Umgebungen, in denen Logins nur als AD-Gruppen angelegt werden (nicht pro Benutzer), fehlt
einem Benutzer, der sich nicht verbinden kann, eines von zwei Dingen: keine seiner AD-Gruppen ist
als Login angelegt (oder der Login ist deaktiviert bzw. hat CONNECT SQL verweigert), oder es
gibt einen nutzbaren Login, aber keinen passenden Datenbankbenutzer in der Zieldatenbank.
Die Funktion loest die AD-Gruppen des Benutzers (direkt und verschachtelt) ueber
Get-sqmADMemberGroups auf, gleicht sie mit den Windows-Gruppen-Logins in sys.server_principals
ab und prueft fuer jeden nutzbaren Login in jeder erreichbaren Datenbank per SID, ob ein
Datenbankbenutzer existiert und in welchen Rollen er ist. Ergebnis ist eine Diagnose
(NoGroups/NoLoginGroup/LoginBlocked/NoDatabaseAccess/.../OK) plus TXT/HTML/CSV-Reports.
Verifiziert: die T-SQL-Abfragen laufen gegen SQL 2022, und ohne gefundene AD-Gruppen liefert die Funktion ein sauberes typisiertes Ergebnis statt abzustuerzen. Den Gruppenabgleich selbst konnte das Labor ohne AD-Domaene nicht durchspielen.
Nachgezogen: doppelte ErrorAction-Bindung (Splat mit ErrorAction=Stop plus explizites
-ErrorAction Stop) entfernt, die unter Windows PowerShell 5.1 abbricht. Der Pre-Push-Hook hatte
das vor dem Push abgefangen.
Get-sqmADMemberGroups zeigte den echten Namen des abgefragten Kontos nichtDer Report loeste den DisplayName fuer jede gefundene Gruppe auf, aber nie fuer die abgefragte Identitaet selbst. Kopf und CSV/HTML zeigten nur den uebergebenen SamAccountName bzw. UPN.
Jetzt wird er auf dieselbe Weise aufgeloest wie bei den Gruppen: Get-ADObject, wenn das AD-Modul
da ist (per LDAPFilter statt Get-ADUser, weil die Identitaet laut Dokumentation Benutzer, Gruppe
oder Computer sein kann), sonst per LDAP/ADSI. Neue Eigenschaft DisplayName am Ergebnisobjekt
und eine Zeile "Real Name" im TXT/HTML-Report.
Get-sqmSupportedCollations und Collation-Auswahl in der GUIGet-sqmSupportedCollations fragt sys.fn_helpcollations() live auf der Zielinstanz ab.
Collations haengen von Version und Edition ab, eine fest verdrahtete Liste wuerde veralten oder
Werte anbieten, die das Ziel gar nicht kennt.
Show-sqmToolGui: -NewCollation von Invoke-sqmCollationChange bekommt einen "List..."-Knopf
(gleiches Muster wie die vorhandenen Browse-Knoepfe). Er oeffnet eine filterbare Auswahl, befuellt
ueber Get-sqmSupportedCollations mit den im Formular eingetragenen -SqlInstance/-SqlCredential,
und schreibt die Auswahl ins vorhandene Textfeld. Wie die GUI Parameterwerte liest, bleibt
unveraendert, Befehlsaufbau und Ausfuehrung sind nicht beruehrt.
Get-sqmDatabaseCollationReport - Collation der Datenbanken gegen die InstanzVergleicht die Collation jeder Datenbank mit der Collation der Instanz (die tempdb immer
verwendet) und warnt bei Abweichungen. Die fuehren zu "Cannot resolve collation conflict" bei
tempdb-Operationen sowie bei datenbankuebergreifenden Joins und Vergleichen. Schreibt TXT- und
HTML-Reports wie die uebrigen Get-sqm*Report-Funktionen.
Register-sqmAuditSession, Extended Events session for the login/database/metadata audit gapCreates (if missing) and starts a dedicated Extended Events session covering the categories
a previous blog post on SQL Server auditing methods flagged as a gap: -FailedLogins,
-SuccessfulLogins, -DatabaseCreated, -DatabaseDropped, -MetadataChanges (schema-level
DDL), each independently selectable, plus -All. No new reader was written - every event
collects the same standard actions (database_name, username, sql_text, etc.)
Invoke-sqmExtendedEvents -Read already knows how to surface.
Every predicate was verified live against DEV01 rather than assumed, and two assumptions turned out to be wrong:
sys.dm_xe_objects actually has dedicated database_created/database_dropped events
(the auditing blog post claimed no such clean event exists for CREATE/DROP DATABASE - that
claim was incorrect and needs fixing in the article).object_created/object_altered/object_deleted fire twice per statement (start and
commit/rollback) - filtered to ddl_phase = 1 (commit) so nothing is double-counted.-DatabaseCreated/-DatabaseDropped to a specific database name
via a WHERE predicate does not work - neither the collected database_name action (it
reflects the connecting session's existing context, not the database being created/dropped)
nor a direct reference to the event's own field produces a working filter; both were tried
and both silently matched nothing. These two categories are always instance-wide by design,
not by omission, and the function warns if -TargetDatabase is supplied without
-MetadataChanges (the one category scoping actually works for, confirmed live: an
unrelated database's DDL was not captured when scoped).-MetadataChanges excludes temp objects (#...) and SQL Server's own auto-generated
statistics objects (_WA_Sys_...) by default (-IncludeSystemGeneratedObjects to capture
them anyway) - both showed up as pure noise in live testing, including from dbatools' own
connection-housekeeping temp tables.Live-verified end to end against DEV01: idempotency (-All twice reports Created then
Unchanged, no session recreation), a failed login, a successful login, CREATE DATABASE,
scoped CREATE/ALTER/DROP TABLE, an out-of-scope table correctly excluded, and
DROP DATABASE, all correctly captured and read back via Invoke-sqmExtendedEvents -Read.
Get-sqmErrorLog, read and categorize the SQL Server error log with ready-made filtersWraps dbatools' Get-DbaErrorLog and adds what it does not provide on its own: switches for the
event types that come up in almost every troubleshooting session instead of everyone re-inventing
the same -match pattern - -FailedLogins, -SuccessfulLogins, -Logins (both), -Backups,
-Restores, -Errors (internal Error: n, Severity: n, State: n. entries), -Shutdowns,
-Startups, -CorruptionEvents, -IOErrors, -MemoryPressure, -ServiceBrokerEvents, plus
-Database, -Since/-Before, -LogNumber (current log and/or archives), -Top, and a
freeform -Pattern that combines with any other filter or works standalone.
Login detection (-FailedLogins/-SuccessfulLogins) is language-neutral: the message templates
for 18456/18453/18454 are read from sys.messages on the target instance and turned into regular
expressions, the same approach Get-sqmLoginLastAccess already uses, so a non-English instance is
matched correctly instead of only ever finding the English wording. The remaining categories match
well-known English message text; the module does not guess localized strings it can't verify, and
-Pattern is the documented escape hatch for anything else or for a localized instance.
Every matched entry is returned with a Category label even with no filter applied, and exports
to CSV/HTML like the other reporting functions (-OutputPath, -NoOpen, Copy-sqmToCentralPath).
Get-sqmDeadlockReport silently found 0 deadlocks even when they existedConfirmed on a machine where the (independently proven) DeadlockCollector T-SQL solution listed
deadlocks from system_health but Get-sqmDeadlockReport reported none. The ring-buffer query
pulled back the whole <event> element (xdr.query('.')) and PowerShell then tried to navigate to
the deadlock node via $dlXml.event.'data'.value.'deadlock' - if <event> has more than one
<data> child (as xml_deadlock_report can), $dlXml.event.'data' becomes an array instead of a
single element and the property chase silently returns nothing (no error, no exception - just 0
results). DeadlockCollector never had this problem because it extracts the <deadlock> node
inside T-SQL via a relative XQuery (xdr.query('(data/value/deadlock)[1]')) instead of leaving
the navigation to PowerShell's XML object model. Get-sqmDeadlockReport now does the same -
queries for (data/value/deadlock)[1] directly, so $dl.DeadlockGraph is already the <deadlock>
node and no further PowerShell-side navigation is needed.
-OutputPath or a Files parameterShow-sqmToolGui's Browse... buttons (OpenFileDialog/SaveFileDialog/FolderBrowserDialog) set the
textbox's .Text and then explicitly called & $updatePreview right after. .Text already fires
Add_TextChanged($updatePreview), so $updatePreview was invoked twice within the same
click-handler call stack; that reentrant re-invocation of the same scriptblock (captured as a raw
.NET event handler) corrupted it, and the next & $updatePreview failed with "The expression after
'&' in a pipeline element produced an object that was not valid. It must result in a command name,
a script block, or a CommandInfo object." Removed the redundant explicit calls - the TextChanged
handler already covers it.
MasterDbObjectWhitelist default was missing CommandLogOla Hallengren's maintenance solution creates a CommandLog table in master by default (not just
the four stored procedures) - left off the default whitelist added in 1.9.110.0. Added, so a
standard Ola install doesn't get its own logging table flagged/dropped out of the box.
Get-sqmMasterDbCustomObjects / Remove-sqmMasterDbCustomObjects, clean up objects accidentally created in masterUsers occasionally create tables, views, procedures, or functions directly in master by
accident (wrong database selected in SSMS, a script run without USE). Unlike a user database,
master has no natural owner for "this shouldn't be here."
Get-sqmMasterDbCustomObjects reads sys.objects in master for tables/views/procedures/functions
(U/V/P/PC/FN/IF/TF/FS/FT), excludes genuine Microsoft-shipped objects
(is_ms_shipped) and anything matching the new MasterDbObjectWhitelist module configuration
(wildcards allowed), and reports the rest as a TXT/CSV/HTML report.
Remove-sqmMasterDbCustomObjects drops what it finds - re-detects live per instance (not from a
possibly stale pipeline object), issues the correct DROP TABLE/DROP VIEW/DROP PROCEDURE/
DROP FUNCTION per object type, each wrapped in its own try/catch. Full -WhatIf/-Confirm
support (ConfirmImpact = 'High'), CSV changelog. There is no per-call bypass of the whitelist -
edit the configuration first if an object should be exempt.
New config key MasterDbObjectWhitelist (Get-sqmConfig -Key 'MasterDbObjectWhitelist' /
Set-sqmConfig -MasterDbObjectWhitelist @(...)), defaulting to the standard maintenance-script
family: sp_Blitz, sp_BlitzBackups, sp_BlitzCache, sp_BlitzFirst, sp_BlitzIndex,
sp_BlitzLock, sp_BlitzWho, CommandExecute, DatabaseBackup, DatabaseIntegrityCheck,
IndexOptimize, sp_WhoIsActive, sp_BackRestRemain.
Live-verified end to end against DEV01: created a table/view/procedure/function directly in
master, confirmed all four were detected and correctly typed, confirmed -WhatIf changed
nothing, confirmed targeted removal by -ObjectName and bulk removal both worked, confirmed a
final scan came back clean, and confirmed whitelisted names are never flagged.
New shared private helper Get-sqmDatabaseTrustIsolationMap (Private\Invoke-sqmHelpers.ps1) reads
is_trustworthy_on, is_read_committed_snapshot_on, and snapshot_isolation_state_desc from
sys.databases for an entire instance in one query, and derives a single readable IsolationLevel
string (READ_COMMITTED_SNAPSHOT, READ_COMMITTED (SNAPSHOT allowed), READ_COMMITTED (default),
or both combined). Built as a dedicated raw-SQL query rather than trusting dbatools' Get-DbaDatabase
SMO object: .Trustworthy is reliably populated, but .ReadCommittedSnapshot comes back $null on a
standard Get-DbaDatabase call (SMO does not eager-load it) - confirmed live on DEV01, where a naive
$db.ReadCommittedSnapshot read would have silently reported every database as RCSI-off regardless of
the real setting.
Wired into every function that reports one row per database: Get-sqmDbOwnerRisk (new IsolationLevel
column, TrustworthyOn already existed), Get-sqmDatabaseHealth, Export-sqmDatabaseDocumentation,
Invoke-sqmInstanceInventory, Invoke-sqmSetupReport, Export-sqmServerConfiguration (new
IsolationLevel, Trustworthy already existed), and Compare-sqmServerConfiguration -CompareDatabases
(now also flags Trustworthy drift as Critical and isolation-level drift as Warning between source
and target). Live-verified end to end against DEV01's 22 databases for all seven functions.
Get-sqmDbOwnerRisk / Repair-sqmDbOwnerRisk, db_owner privilege-escalation audit and fixdb_owner is functionally equivalent to CONTROL on the database: members can create triggers and
procedures with EXECUTE AS OWNER, which run as the database owner (dbo), not the caller. If a
database has TRUSTWORTHY = ON and its owner maps to a sysadmin-privileged login (common, since
databases are usually created by an admin/setup account), any db_owner member can escalate to full
instance control via a single CREATE PROCEDURE ... WITH EXECUTE AS OWNER.
Get-sqmDbOwnerRisk finds, per database, any non-dbo db_owner members and whether that escalation
path is actually open (TRUSTWORTHY + sysadmin owner), and writes a TXT/CSV/HTML report - green
rows for databases with no unexpected db_owner members, red rows for the rest (Warning if just the
membership is wrong, Critical if the full escalation path is open).
Repair-sqmDbOwnerRisk fixes what it finds: removes the offending db_owner membership, adds
db_datareader + db_datawriter, creates a custom db_execute role (CREATE ROLE ... AUTHORIZATION
dbo) if it doesn't exist yet, grants that role EXECUTE on every user stored procedure in the
database (skipped if there are none), and adds the login to it. Accepts Get-sqmDbOwnerRisk's
pipeline output directly. Full -WhatIf/-Confirm support (ConfirmImpact = 'High'), per-member
error isolation so one failing login doesn't stop the rest, and a CSV changelog per instance.
Background: db_owner Risks: Trigger Creation, Ownership Chaining, and the Path to sysadmin
Get-sqmAlwaysOnQueueStatus, pollable redo/send queue status, no report filesGet-sqmAlwaysOnHealthReport already reads redo queue / send queue per replica and database via
sys.dm_hadr_database_replica_states, but it always writes TXT/CSV/HTML report files on every
call - wrong shape for something called every few minutes by a poller (SQLLiveDiagnose central
poller in particular). The DMV query and threshold/status scoring (redo/send MB conversion,
OverallStatus OK/Warning/Critical) moved into a new private helper,
Get-sqmAlwaysOnQueueSnapshot, shared by both functions - Get-sqmAlwaysOnHealthReport's own
behavior (AutoSeed tracking, TXT/CSV/HTML writing, thresholds) is unchanged, it just sources its
per-row data from the helper now instead of an inline query. Get-sqmAlwaysOnQueueStatus is the
new lightweight sibling: same -SqlInstance/-SqlCredential/-MaxRedoQueueMB/-MaxSendQueueMB
parameters, returns the row objects directly, no file I/O. An instance without any availability
groups returns an empty array, not an error.
Get-sqmADGroupMembers did not resolve real AD display namesBrought the non-recursive Get-sqmADGroupMembers in line with Get-sqmADGroupMembersRecursive,
which already resolved this correctly. Method 1 (Get-ADGroupMember) only returns the CN/Name
(often just the login), not the displayName attribute - user members now get their real
displayName loaded via Get-ADUser -Properties DisplayName. Method 2 (LDAP fallback) had a
latent bug: sAMAccountName and displayName were read via InvokeGet inside the same try,
so a group member whose AD object simply has no displayName attribute threw before the already-read
sAMAccountName was ever applied, silently falling back to the CN for both fields. Each attribute
is now read individually and tolerantly, with the same displayName -> cn -> sAMAccountName
fallback chain used elsewhere in the module.
Unlock-sqmSqlLogin, unlock a CHECK_POLICY-locked SQL loginCovers the two standard cases for a SQL-auth login locked out by repeated bad-password
attempts under CHECK_POLICY = ON: unlock without knowing/touching the current password
(known or unknown password, caller just wants back in), or a real password reset when the
password is genuinely lost. T-SQL has no standalone ALTER LOGIN ... UNLOCK - UNLOCK
only works combined with PASSWORD =. Without -NewPassword the function instead uses
the documented CHECK_POLICY = OFF / = ON toggle, which clears the Windows-managed
lockout flag without touching the password hash at all. Neither path restates
CHECK_POLICY/CHECK_EXPIRATION in the ALTER LOGIN statement, so SQL Server leaves
both exactly as they were; the function verifies that via sys.sql_logins afterward
instead of trusting "no exception = nothing changed". -NewPassword (SecureString) does
a real PASSWORD = @newpwd UNLOCK reset, passed as a query parameter and never logged;
-MustChange requires CHECK_EXPIRATION = ON on the login and fails fast with a clear
message otherwise instead of surfacing SQL Server's raw error. Already-unlocked logins are
a no-op (Status = 'AlreadyUnlocked').
Set-sqmSqlDirectoryPermissions alias for Invoke-sqmNtfsSetupA request to "add a function that sets permissions for SQL Server and SQL Agent on the
directories used by SQL Server" turned out to already exist as Invoke-sqmNtfsSetup
(auto-discovers the Engine/Agent service accounts via Get-DbaService and the Data/Log/
Backup/TempDB directories via Get-DbaDefaultPath + sys.master_files, backs up the ACLs
first, then grants FullControl/Modify). It just wasn't found because the Invoke-* verb and
"NtfsSetup" name give no hint that it's a permissions function. Added Set-sqmSqlDirectoryPermissions
as an alias (Set-Alias at the end of Public\Invoke-sqmNtfsSetup.ps1, registered in
AliasesToExport) so it surfaces under Get-Command -Noun *Permission* without touching the
existing function name, exports, docs, or any Agent job/script that already calls it by its
original name.
Get-sqmWaitStatistics silently swallowed SQL login/connection failures in the GUIBug report: Show-sqmToolGui crashed with "Cannot index into a null array" while testing
Get-sqmWaitStatistics against a SQL-authenticated login (gui-launch.log, three occurrences
within one session). Reproduced the SQL-login path headlessly ($form.Show() + $btnRun.PerformClick()
instead of ShowDialog(), so the Run button's real Click handler runs without blocking on a
modal dialog) with both a valid and an invalid DEV01 credential. The invalid-credential case
never threw a null-array error, but it did surface a real, separate, and reproducible bug:
Invoke-DbaQuery @connParams -Database master -Query $waitSql -ErrorAction Stop does not
actually throw on a failed login - dbatools just Write-Warnings and returns $null unless
-EnableException is passed - so the failed-login case fell through to the "instance returned
zero waits" path and the GUI showed "(No result / no output)" with no indication the login had
failed at all. Fixed by adding -EnableException:$true to that call, matching the pattern
already used for the connectivity-check query in Get-sqmAlwaysOnHealthReport /
Get-sqmDistributedAgHealth. Also extended Show-sqmToolGui's connection-error detection
regex to recognize German SQL Server error text (e.g. "Fehler bei der Anmeldung") - the
existing patterns were English-only, so a German-locale instance's login failure fell through
to the generic ERROR: ... line instead of the clearer "SQL CONNECTION FAILED" message.
Could not reproduce the literal null-array crash itself despite testing successful login,
failed login (wrong password), and the default local-instance prefill combined with a
mismatched credential - none of those hit it. Hardened the two array-index sites in the
Run button's own mandatory-parameter validation ($missing[0] / $paramSets[$i], the only
places in that handler that index by integer rather than by hashtable key) against a $null
array defensively, since a crash there would bypass the handler's own try/catch entirely (it
only wraps the actual command invocation, not the validation step before it) and reach the
top-level unhandled-exception handler with no script context. Also enhanced that top-level
handler in Start-sqmToolGui.ps1 to log $e.Exception.ErrorRecord.InvocationInfo.PositionMessage
in addition to the bare .StackTrace - the interpreter-frame stack trace alone carries no file
or line number, which is why the original crash log couldn't be traced back to a cause. If this
recurs, gui-launch.log will now show the exact line.
Show-sqmToolGuiDrei Verbesserungen an der Parameter-Erfassung der GUI:
File/Files endet (z. B. -BackupFile/-BackupFiles
bei Invoke-sqmRestoreDatabase), bekommen jetzt einen "Browse..."-Button, der einen
OpenFileDialog oeffnet statt den Pfad von Hand eintippen zu muessen. Bei
Array-Parametern ist Mehrfachauswahl aktiv; die gewaehlten Dateien werden genauso
Komma-separiert in die Textbox geschrieben, wie sie das bestehende Splitting in
$buildCommand/Run bereits erwartet.-OutputPath wird jetzt mit demselben Standardverzeichnis vorbelegt, das die
jeweilige Funktion sonst intern verwenden wuerde (Get-sqmDefaultOutputPath
dahinter). Da dieser Default in praktisch allen betroffenen Funktionen ein Aufruf
eines privaten, nicht exportierten Modul-Helfers ist, reicht ein einfaches
InvokeScript() im GUI-Runspace nicht - der aus dem Funktions-AST extrahierte
Default-Ausdruck wird deshalb per $cmd.Module.NewBoundScriptBlock() im
Session-State des Moduls selbst ausgewertet (live an vier realen Varianten
verifiziert: Get-sqmWaitStatistics, Get-sqmAgentJobHistory,
Enable-sqmMonitoringAccess, Install-sqmSsrsReportServer). Der zugehoerige
"Browse..."-Button oeffnet einen FolderBrowserDialog fuer Verzeichnis-Defaults,
bzw. einen SaveFileDialog, wenn der Default (wie bei Get-sqmAgentJobHistory)
bereits ein vollstaendiger Dateiname mit Endung ist.Die neue AST-/Default-Auswertungslogik wurde gegen das echte, importierte Modul
getestet (vier Funktionen, alle vier OutputPath-Varianten liefern den korrekten
Pfad). Das WinForms-Layout selbst (Button-Platzierung, Zeilenbreite) liess sich in
dieser Umgebung nicht interaktiv per Klick verifizieren, da ShowDialog() blockiert
und kein Desktop-Screenshot-Werkzeug zur Verfuegung steht - rein per Code-Review gegen
das bestehende Layout-Muster abgesichert.
Get-sqmBlockingHistory + Register-sqmBlockedProcessMonitor - vergangene Blocking-VorfaelleGemeldet: Get-sqmBlockingReport zeigt nur Blocking im Moment des Aufrufs, keine
zurueckliegenden Waits. Neue Funktion Get-sqmBlockingHistory liest dafuer
blocked_process_report-Events aus Extended-Events-Ring-Buffern (SPID, Login, Host,
Programm, Lock-Modus, Wait-Resource, Wartezeit, Statement-Ausschnitt je Vorfall).
Zwei Design-Annahmen live widerlegt, bevor der finale Stand stand:
system_health-Session enthaelt blocked_process_report nicht
zuverlaessig - auf einer Standardinstallation (DEV01) fehlt das Event komplett in der
Session-Definition (sys.server_event_session_events gepruef). Ohne lauschendes Ziel
verpuffen die Events, selbst bei aktiviertem blocked process threshold (s). Neue
Funktion Register-sqmBlockedProcessMonitor legt daher bei Bedarf eine eigene,
dauerhafte Session sqm_BlockedProcessMonitor an (blocked_process_report +
xml_deadlock_report, ring_buffer-Target, optional -IncludeFileTarget fuer
laengere Aufbewahrung via event_file). Get-sqmBlockingHistory ruft das automatisch
auf (-SkipMonitorSetup zum Abschalten) und liest zusaetzlich weiterhin
system_health mit, falls das dort ausnahmsweise doch vorhanden ist.'blocked process threshold (s)' (mit
Einheiten-Suffix) - sp_configure/sys.configurations kennen kein
'blocked process threshold' ohne Suffix. Mit einem echten, live via zwei
Hintergrund-Connections erzeugten Blocking-Vorfall verifiziert..value() auf ein zuvor per .query()
materialisiertes XML-Zwischenergebnis lieferte fuer jede Spalte DBNull - trotz
nachweislich vollstaendiger Roh-XML. Live an einem echten, ~12 Sekunden gehaltenen
Blocking-Vorfall auf DEV01 gefunden (BlockedWaitMs-Division warf
System.DBNull does not contain a method named 'op_Division'). Fix: der komplette
XPath laeuft jetzt in einem Zug direkt gegen die Ereignisspalte statt ueber eine
Zwischenspalte - danach lieferten alle drei erzeugten Testereignisse exakt die
erwarteten Werte (SPIDs, Wartezeiten 3.5/9.6/12.6s, Statements, Lock-Modus).
Zusaetzlich gehaertet: Invoke-DbaQuery liefert SQL NULL als [DBNull], nicht als
PowerShell $null - reine Truthy-Pruefungen erkennen das nicht.Alle Testartefakte (Testdatenbank, temporaere Monitor-Session, Threshold/Advanced-Options) nach jedem Testlauf von DEV01 entfernt, Ausgangszustand wiederhergestellt.
Sync-sqmBackupExcludeTable, AG-Propagierung konnte die Primary ueberschreibenGemeldet: sind Aenderungen an sqm_BackupExclude zuverlaessig auf allen AG-Secondaries? Antwort
bis zu diesem Fix: nein, nicht ganz. Die Propagierung nutzte sys.availability_replicas WHERE
replica_server_name <> @@SERVERNAME - das listet nur "alle anderen Knoten", ohne Rollenbezug.
Der 30-Minuten-Sync-Job laeuft auf allen AG-Knoten, auch Secondaries. Feuert eine Secondary's
Zyklus zufaellig vor dem naechsten Zyklus der Primary, pusht die Secondary ihren eigenen, noch
veralteten Stand per MERGE auf die Primary - und macht damit eine gerade erst per
Show-sqmBackupExcludeForm gemachte Aenderung rueckgaengig, ohne dass es auffaellt. Passt zum
beobachteten Muster mehrfach hin- und her-getoggelter IsActive-Werte auf BLBNBGFATDBA3.
Fix: vor jeder Propagierung wird jetzt geprueft, ob die aktuelle Instanz tatsaechlich die
AG-Primary ist (sys.dm_hadr_availability_group_states.primary_replica = @@SERVERNAME) - exakt
dieselbe Technik wie der Primary-Fix in Sync-sqmLoginsToAlwaysOn (v1.8.3.0). Ist die Instanz
nicht Primary, wird die Propagierung fuer diesen Lauf komplett uebersprungen (keine Secondary
pusht mehr ungefragt ihren eigenen Stand nach aussen).
Gegen DEV01 verifiziert: neue Pruefquery laeuft fehlerfrei (liefert korrekt IsPrimary=0 auf
dieser Nicht-AG-Instanz), Sync-sqmBackupExcludeTable laeuft weiterhin fehlerfrei end-to-end.
Das eigentliche Verhindern-Szenario (Secondary ueberschreibt Primary) konnte im Lab nicht in
einer echten Mehrknoten-AG nachgestellt werden - DEV01 hat kein AlwaysOn.
New-sqmOlaUsrDbBackupJob -UseExcludeTable, Cursor statt einer langen AusschlusslisteGemeldet: die bisherige Umsetzung baut eine einzige '-DatabaseName,-DatabaseName,...'-Liste
und haengt sie an Olas @Databases-Parameter an. Auf Instanzen mit vielen Ausschluessen
(Praxisfall: ~90 Zeilen in sqm_BackupExclude) wird diese Liste unpraktikabel lang.
Die generierte Prozedur (sqm_Run_<Jobname>) cursort jetzt einzeln durch jede
Kandidaten-Datenbank (aufgeloest aus -Databases: USER_DATABASES, ALL_DATABASES oder eine
explizite kommagetrennte Liste via STRING_SPLIT) und prueft pro Datenbank direkt gegen
master.dbo.sqm_BackupExclude (IsActive = 0 AND IsOrphaned = 0 = uebersprungen). Nur
nicht-ausgeschlossene Datenbanken werden einzeln per EXECUTE master.dbo.DatabaseBackup
@Databases = @dbName, ... gesichert - es wird nie eine lange Liste zusammengebaut. Fehlt die
Tabelle (OBJECT_ID(...) IS NULL), wird jede Kandidaten-Datenbank gesichert.
Show-sqmBackupExcludeForm: die Warnzeile zur Laenge der Exclusion-Liste (Schwellwerte 1500/1900
Zeichen, RAISERROR-2047-Warnung) ist damit hinfaellig und wurde entfernt (Update-
ExclusionLengthIndicator, $pWarn-Panel).
Live gegen DEV01 verifiziert: generierte Prozedur (FULL + LOG) kompiliert und wird korrekt als
SQL-Agent-Job angelegt; isolierter Logik-Test (Stub statt echtem DatabaseBackup) mit einer
temporaer auf IsActive=0 gesetzten Testdatenbank (AdventureWorks) bestaetigt: von 18
Kandidaten-Datenbanken wurden korrekt 17 "gesichert", die ausgeschlossene fehlt im Ergebnis.
Alle Testartefakte (Jobs, Prozeduren, Stub, Testtabelle) nach dem Test entfernt, Ausgangszustand
wiederhergestellt.
Invoke-sqmUserDatabaseBackup -UseExcludeTable hatte die IsActive-Polaritaet invertiertmaster.dbo.sqm_BackupExclude wird von zwei Funktionen gelesen: New-sqmOlaUsrDbBackupJob
(produktiver Standard, Ola-Hallengren-basiert) interpretiert IsActive=1 korrekt als "diese
Datenbank wird gesichert" (Default fuer neu von Sync-sqmBackupExcludeTable erkannte
Datenbanken; seit v1.8.13.0 so verifiziert) und schliesst nur bei IsActive=0 AND
IsOrphaned=0 aus. Invoke-sqmUserDatabaseBackup -UseExcludeTable filterte bislang jedoch auf
IsActive = 1 AND IsOrphaned = 0 - also genau umgekehrt.
Praktische Folge: unter Invoke-sqmUserDatabaseBackup -UseExcludeTable wurde jede neu
erkannte Datenbank ab dem ersten Sync-sqmBackupExcludeTable-Lauf automatisch vom Backup
ausgeschlossen (Default-Wert IsActive=1), bis ein Admin die Checkbox in
Show-sqmBackupExcludeForm manuell umschaltet - ein stiller Datenverlust-Risikofall bei
gemischtem Einsatz beider Backup-Funktionen gegen dieselbe Tabelle.
Fix: Query auf WHERE IsActive = 0 AND IsOrphaned = 0 korrigiert, Log-Meldungen und
Doku-Kommentare an die jetzt uebereinstimmende Semantik beider Funktionen angepasst. Das
GUI-Label "Aktiv (Backup)" in Show-sqmBackupExcludeForm war bereits korrekt und wurde nicht
veraendert.
Get-sqmDatabaseHealth zeigt Backup-Ausschluss (sqm_BackupExclude)Gemeldet: eine Datenbank ohne (aktuelles) Backup im Health-Report ist im Report nicht von einer
Datenbank zu unterscheiden, die absichtlich ueber master.dbo.sqm_BackupExclude vom Backup
ausgenommen ist (siehe New-sqmOlaUsrDbBackupJob / Invoke-sqmUserDatabaseBackup
-UseExcludeTable). Neue Spalte "Backup-Ausschluss" im HTML-Bericht ("Ausgeschlossen (<Grund>)"
wenn gesetzt), neue Properties ExcludedFromBackup/ExcludeReason je Datenbank im
Rueckgabeobjekt/CSV, sowie eine Zusammenfassungszeile im TXT-Bericht.
Semantik: IsActive = 0 AND IsOrphaned = 0 = ausgeschlossen (IsActive=1 heisst "wird
gesichert", Default fuer neu erkannte Datenbanken) - deckungsgleich mit der Beschriftung "Aktiv
(Backup)" in Show-sqmBackupExcludeForm sowie mit New-sqmOlaUsrDbBackupJob. Fehlt die
Tabelle, werden keine Ausschluesse angenommen.
Korrektur 1.9.98.0: die urspruengliche Fassung dieser Funktion (und dieses Eintrags) hatte die
Polaritaet fuer 1.9.97.0 fehlerhaft von Invoke-sqmUserDatabaseBackup uebernommen, dessen
Query selbst invertiert war - siehe oben.
OverallStatus bleibt unveraendert - Backup-Aktualitaet fliesst schon bisher nicht in die
Bewertung ein (separate, bereits bestehende Luecke, hier nicht mit angefasst). Mit gemockten
dbatools-Daten via Pester verifiziert (3 neue Tests: Flag+Grund gesetzt, unbeteiligte DB bleibt
unmarkiert, HTML enthaelt Spalte+Text).
Get-sqmDatabaseSpaceReport - Fuellstand der Datenbankdateien (Data + Log)Bisher gab es keinen Report fuer den tatsaechlichen Fuellstand einer Datenbank (belegte Datenseiten
vs. allokierte Dateigroesse) - nur benachbarte, aber andere Kennzahlen: Get-sqmDiskSpaceReport
(freier Platz auf dem Windows-Laufwerk) und Get-sqmAutoGrowthReport (allokierte Dateigroesse +
Autogrowth-Konfiguration). Neue Funktion liest pro Datenbank Data- und Log-Dateien getrennt aus,
aggregiert je Datenbank zu einem Fuellstand in % und markiert Warning/Critical ab konfigurierbaren
Schwellwerten (-WarnThresholdPct 80 / -CriticalThresholdPct 90, Default). CSV (Datei-Detail) +
HTML (Datenbank-Zusammenfassung, farbcodiert) im etablierten Report-Format, Default--OutputPath
nach demselben Muster wie die uebrigen Reports (Get-sqmDefaultOutputPath + DatabaseSpaceReport).
Aufbauend auf dbatools' Get-DbaDbSpace. Fallstrick beim Bau entdeckt: Get-DbaDbSpaces eigener
-IncludeSystemDBs-Switch ist in der hier installierten dbatools-Version (2.8.4) deprecated und
bricht mit Stop-Function ab, ohne etwas zurueckzugeben - -IncludeSystemDatabases steuert die
System-DB-Aufnahme deshalb ueber -ExcludeDatabase (master/model/msdb/tempdb), nicht ueber den
kaputten Switch. Mit gemockten dbatools-Daten via Pester verifiziert (8 Tests: Schwellwert-Logik,
Critical-/OK-Einstufung, CSV/HTML-Erzeugung inkl. Default-Pfad).
-OutputPathVollstaendige Durchsicht aller oeffentlichen Funktionen mit -OutputPath-Parameter nach den
Einzelfixes fuer Get-sqmBlockingReport (v1.9.93.0) und Get-sqmDeadlockReport (v1.9.94.0): 23
Funktionen hatten einen unbedingten [string]$OutputPath,-Parameter ohne Default. 17 davon waren
bereits korrekt (Default schon im begin-Block gesetzt, z. B. Invoke-sqmPerfBaseline,
Get-sqmCertificateReport, Invoke-sqmRestoreTest) oder haben -OutputPath bewusst anders belegt
(Export-sqmDatabaseSettings/Export-sqmDatabaseLogins: Mandatory, da Kennwort-Hashes/Settings
ohne explizites Ziel nicht exportiert werden sollen; Set-sqmConfig: setzt den Konfigurationswert
selbst, kein Report-Pfad). Sechs Funktionen fehlte der Default tatsaechlich - Fix nach dem
etablierten Muster (Join-Path (Get-sqmDefaultOutputPath) '<Unterordner>'):
- Get-sqmLoginLastAccess → ...\LoginLastAccess
- Get-sqmLoginPermissions → ...\LoginPermissions
- Get-sqmTempDbRecommendation → ...\TempDbRecommendation
- Get-sqmIndexFragmentation → ...\IndexFragmentation
- Invoke-sqmPatchAnalysis → ...\PatchAnalysis
- Get-sqmAgentJobHistory → Sonderfall: -OutputPath ist dort ein kompletter Dateipfad (nicht
Verzeichnis), Default ist deshalb ein vollstaendiger, zeitgestempelter Dateiname unter
...\AgentJobHistory; Schreiblogik legt das uebergeordnete Verzeichnis jetzt bei Bedarf an.
Alle sechs mit gemockten dbatools-Daten via Pester verifiziert: CSV/HTML landen jetzt ohne
explizites -OutputPath im Default-Ordner.
Get-sqmDeadlockReport hatte keinen Default-Wert fuer -OutputPathGleicher Fehler wie eben bei Get-sqmBlockingReport (siehe dort): ohne explizites -OutputPath
wurden nie XDL-Graphen oder ein HTML-Summary geschrieben. Fix nach demselben Muster
(Join-Path (Get-sqmDefaultOutputPath) 'DeadlockReport'). Anmerkung: mindestens 23 weitere
oeffentliche Funktionen haben denselben unbedingten [string]$OutputPath,-Parameter ohne
Default - noch nicht angefasst, da nicht angefragt.
Get-sqmBlockingReport hatte keinen Default-Wert fuer -OutputPathWie schon bei den fuenf Batch-B3-Funktionen in v1.9.80.0 (siehe dort) fehlte -OutputPath ein
Standardwert - ohne explizite Angabe wurde nie ein CSV/HTML-Report geschrieben. Fix nach dem
etablierten Muster (Join-Path (Get-sqmDefaultOutputPath) 'BlockingReport'). Die bestehende
Bedingung -and $blockedSessions.Count -gt 0 bleibt unveraendert (analog Get-sqmMissingIndexes:
bewusst kein leerer Report, wenn keine Blockierung vorliegt).
Invoke-sqmSplunkConfiguration - Modus Remove zum Rueckbau der Splunk-KonfigurationBisher konnte die Funktion die Splunk-Konfiguration nur einrichten (Set) oder pruefen (Test),
aber nicht wieder entfernen. Neuer Modus Remove: loescht alle MSSQLn_Log-Umgebungsvariablen
(unabhaengig von noch vorhandenen SQL-Instanzen, da beim Ausbau/Deinstallieren einer Instanz die
Registry-Eintraege bereits weg sein koennen) und stoppt den SplunkForwarder-Dienst. Funktioniert
lokal sowie ueber die bestehenden -Remote/-ComputerList-Wege, da die interne Kernlogik jetzt
den Modus als String statt als Set/Test-Bool durchreicht.
Invoke-sqmSplunkConfiguration - fehlende Administratorrechte wurden als Success gemeldet_sqmSplunk_LocalCore bricht bei fehlenden Administratorrechten (Modus Set/Remove) fruehzeitig
ab, aber der oeffentliche Rueckgabewert wurde davon unabhaengig nur anhand des tatsaechlichen
Ist-Zustands (vorhandene Env-Vars/Dienststatus) gebildet - dadurch meldete die Funktion faelschlich
Status = Success bzw. NotConfigured, obwohl gar nichts ausgefuehrt wurde. _sqmSplunk_LocalCore
gibt jetzt $true/$false zurueck; ein $false fuehrt lokal zu Status = Error mit der
eigentlichen Fehlermeldung. Im Remote-/List-Pfad wird der Rueckgabewert jetzt abgefangen (statt
unterhalb in $results einzusickern) und markiert den betroffenen Zielrechner als
Fehler (siehe Zielrechner-Log).
Der -NotifyOwner-Nachrichtentext benannte SPID/Datenbank/Instanz und dass die Session "beendet"
wurde, sagte dem Empfaenger aber nicht, was das konkret fuer ihn bedeutet: Verbindung geschlossen,
laufende Transaktion automatisch zurueckgerollt, ggf. erneut anmelden. Aus einem Livetest gegen
DEV01 als zu wenig aussagekraeftig zurueckgemeldet. Text erweitert, damit die Konsequenz explizit
benannt wird statt nur der beendete Zustand.
Stop-sqmSqlProcess - SQL-Session beenden mit optionaler BesitzerbenachrichtigungBisher gab es in sqmSQLTool keine eigene Funktion zum gezielten Beenden einzelner SQL-Sessions
(nur den internen KILL-Aufruf in Invoke-sqmRestoreDatabase fuer Restore-Retries).
Stop-sqmSqlProcess liest vor dem Kill Login/Hostname/Programm/Datenbank der Ziel-SPID(s) aus
sys.dm_exec_sessions, beendet sie per Stop-DbaProcess und sendet bei -NotifyOwner
anschliessend eine Nachricht per msg.exe an den Hostnamen der Session ("net send" existiert
seit Vista nicht mehr). -NotifyOwner ist standardmaessig AUS, damit automatisierte/Job-Kills
keine Popups ausloesen. Ein Fehlschlag der Benachrichtigung (Host offline, keine Rechte, kein
Hostname bekannt) bricht den bereits erfolgten Kill nicht ab, sondern wird nur als eigenes
NotifyStatus-Feld im Ergebnis protokolliert. Unterstuetzt Mehrfach-SPIDs in einem Aufruf (z.B.
direkt aus Get-sqmBlockingReport gespeist) sowie -WhatIf/-Confirm (ConfirmImpact = High).
msg.exe-Zustellung selbst wurde noch nicht live gegen die Workgroup-Laborumgebung verifiziert -
host_name ist ein vom Client gemeldeter, nicht verifizierter Wert und bei Verbindungen ueber
Anwendungsserver/Service-Accounts sitzt dort haeufig niemand Interaktives.
Array-Parameter wie [string[]]$Database (z.B. bei Invoke-sqmUserDatabaseBackup) wurden in der
GUI als normale einzeilige Textbox dargestellt. Beim Klick auf "Run" wurde der eingegebene Text
unveraendert als EIN String an den Parameter uebergeben ($params[$pname] = $ctrl.Text). Gab der
Anwender z.B. "DB1, DB2" ein, band PowerShell diesen String als Array mit genau EINEM Element
("DB1, DB2") an den Parameter, statt zwei getrennte Datenbanknamen zu liefern. Die Folge:
Invoke-sqmUserDatabaseBackup fand keine passende Datenbank und meldete "Keine Benutzerdatenbanken
fuer Backup gefunden", obwohl beide Datenbanken existierten. Behoben, indem der Run-Handler und die
Kommandovorschau (Copy to clipboard) den Textbox-Inhalt bei Array-Parametern jetzt an Komma/Semikolon
splitten und ein echtes Array binden; das Parameter-Tooltip weist bei Array-Typen zusaetzlich auf die
Komma-Trennung hin. Live gegen DEV01 verifiziert (vorher: "NotFound"/"NoDatabasesFound" bei
kommagetrennter Eingabe, danach: beide Datenbanken korrekt aufgeloest).
Import-sqmServerConfiguration - Counterpart zu Export-sqmServerConfigurationDer Export erzeugte bisher nur Snapshots (fuer Dokumentation/Vergleich/Rollback), es gab keinen Weg
sie wieder anzuwenden. Import-sqmServerConfiguration liest eine per Export-sqmServerConfiguration
geschriebene JSON-Datei und wendet davon alles an, was auf einer bereits installierten Instanz
tatsaechlich restaurierbar ist: sp_configure (per Set-DbaSpConfigure), BackupDirectory/DefaultFile/
DefaultLog (per Set-DbaDefaultPath), TempDb-Dateigroessen/-Growth (per ALTER DATABASE tempdb
MODIFY FILE, verkleinert nie) und Dienst-StartMode. Aktueller und Zielwert werden vorher verglichen,
sodass nur tatsaechliche Abweichungen angefasst werden - jede Einstellung bekommt eine eigene
Ergebniszeile (Success/Skipped/Failed/WhatIf), analog zu Import-sqmDatabaseLogins. Nicht
restaurierbare Kategorien (Edition/Collation/LoginMode als reine Serverinstallations-Eigenschaften,
DatabaseMail, LinkedServers, das Databases-Inventar) werden als 'Informational' ausgewiesen statt
stillschweigend ignoriert.
Export-sqmDatabaseSettings / Import-sqmDatabaseSettings - Datenbank-Options (SSMS "Options"-Seite)Ergaenzt die bereits vorhandenen Server-weiten Snapshots um die Datenbank-Ebene: erfasst bzw.
restauriert die komplette Options-Seite der SSMS Database Properties (Compatibility Level, Recovery
Model, Page Verify, Target Recovery Time, Delayed Durability, alle ANSI-/ARITHABORT-/AUTO_*-Flags,
Cursor-Default, Parameterization, DB Chaining, Trustworthy, sowie - nur mit
-IncludeExclusiveOptions, da sie aktive Verbindungen trennen koennen - ReadOnly,
ReadCommittedSnapshot und BrokerEnabled). Beide Funktionen teilen sich die Settings-Definition
(Get-sqmDatabaseSettingsDefinition in Private/), damit Export und Import nie auseinanderlaufen.
Export-sqmDatabaseSettings liest alle Datenbanken in einer einzigen Abfrage gegen sys.databases
(kein Connect je Datenbank noetig), Import-sqmDatabaseSettings vergleicht vor jeder Aenderung den
Live-Wert und wendet ausschliesslich Abweichungen per ALTER DATABASE ... SET an - eine
Ergebniszeile pro Einstellung und Datenbank.
Export-sqmServerConfiguration und Export-sqmDatabaseSettingsBeide Export-Funktionen schreiben jetzt zusaetzlich zur JSON-Snapshot-Datei einen HTML-Bericht mit
gleichem Dateinamen (.html statt .json) im sqmSQLTool-Theme (ConvertTo-sqmHtmlReport), gedacht zum
direkten Zeigen beim Kunden statt der rohen JSON-Datei. Export-sqmServerConfiguration listet
sp_configure, Instance Properties, Services, TempDb-Dateien, Database-Mail-Profile und Linked
Servers tabellarisch auf; Export-sqmDatabaseSettings zeigt eine Uebersichtstabelle aller
Datenbanken plus je Datenbank ein aufklappbares Detail (<details>) mit allen erfassten
Options-Werten. Wird wie bei den uebrigen Report-Funktionen des Moduls automatisch geoeffnet, -NoOpen
unterdrueckt das; der Pfad kommt als neues ReportPath-Feld im Rueckgabeobjekt.
Export-sqmServerConfiguration erfasste praktisch keine sp_configure-Werte$server.Configuration ist selbst kein Enumerable (ein foreach darueber liefert nur ein einzelnes
leeres SMO-Objekt) - die eigentlichen ~95 sp_configure-Eintraege liegen unter
$server.Configuration.Properties. Live gegen DEV01 getestet: vor dem Fix enthielt der Snapshot nur
einen leeren SpConfigure-Eintrag, ConfigName war ausserdem auf dieser SMO-Version durchgehend leer
(jetzt Fallback auf DisplayName, das Set-DbaSpConfigure -Name ebenfalls akzeptiert). Ohne diesen Fix
haette Import-sqmServerConfiguration nie etwas zum Anwenden gehabt. Betrifft nur die
SpConfigure-Erfassung, alle anderen Kategorien waren bereits korrekt.
Get-sqmAutoGrowthReport - TXT/HTML-Bericht ergaenztBisher gab Get-sqmAutoGrowthReport nur das Objekt-Array zurueck, ohne Berichtsdateien
zu schreiben (anders als die uebrigen Report-Funktionen im Modul). Jetzt werden - wie bei
Get-sqmDiskSpaceReport - zusaetzlich ein TXT- und ein HTML-Bericht (nach Status sortiert,
farbcodiert) in -OutputPath abgelegt (Default: AutoGrowthReports-Unterordner unter dem
konfigurierten Standard-Ausgabepfad) und per Invoke-sqmOpenReport geoeffnet (-NoOpen
unterdrueckt das).
Die Funktion wird auch intern von Get-sqmFileGrowthHistory als reine Datenquelle
aufgerufen; damit dort nicht bei jedem Lauf zusaetzlich ein eigener AutoGrowth-Bericht
entsteht, ruft Get-sqmFileGrowthHistory jetzt mit dem neuen Schalter -NoReport auf.
Das zurueckgegebene Objekt-Array selbst ist unveraendert (bestehende Aufrufer funktionieren
unveraendert weiter).
Get-sqmFileGrowthHistory - Datei-Wachstum ueber die Zeit aufzeichnen und prognostizierenGet-sqmAutoGrowthReport liefert nur eine Momentaufnahme der AutoGrowth-Konfiguration
(aktuelle Groesse, Growth-Typ/-Wert, MaxSize) - keine Historie und keinen Trend.
Get-sqmFileGrowthHistory schliesst diese Luecke nach dem gleichen Muster (Methode B1)
wie Get-sqmDiskSpaceReport: Bei jedem Lauf wird Get-sqmAutoGrowthReport -Detailed als
alleinige Datenquelle abgefragt und die aktuelle Groesse jeder Datei in eine
Snapshot-Historie pro Instanz (FileGrowthHistory_<Instanz>.json) angehaengt. Eine
lineare Regression ueber das -HistoryDays-Fenster liefert MB/Tag Wachstum je Datei und,
bei begrenzter MaxSize, die Prognose in wie vielen Tagen die Grenze erreicht wird. Die
Regressions-/Konfidenzberechnung selbst wird nicht neu geschrieben, sondern die bereits
vorhandene modulinterne Hilfsfunktion Get-sqmVolumeForecast (aus Get-sqmDiskSpaceReport.ps1,
generisch fuer beliebige Timestamp/Groesse-Zeitreihen) wiederverwendet. Ergebnis wird wie
gewohnt als TXT/CSV/HTML-Bericht abgelegt.
Braucht mindestens -MinDataPoints (Default 5) wiederkehrende Laeufe (z. B. taeglicher
Agent-Job) bevor eine Prognose statt "sammelt noch" ausgegeben wird.
Funktionsanzahl im Modul: 164 -> 165.
Restore-sqmSysadminAccess - Notfall-Wiederherstellung bei komplettem DBA-LockoutBislang gab es im Modul keine Funktion fuer den Fall, dass auf einer Instanz KEIN
funktionierender sysadmin-Login mehr existiert (Logins geloescht, Passwoerter verloren,
versehentlich alle sysadmin-Mitgliedschaften entzogen). Bisher blieb dafuer nur die
manuelle Prozedur: Dienst stoppen, mit Startparameter -m"..." (Single-User-Mode) neu
starten, per sqlcmd als lokaler Administrator verbinden (im Single-User-Mode implizit
sysadmin), Login anlegen/Passwort zuruecksetzen, sp_addrolemember/ALTER SERVER ROLE,
Dienst wieder normal starten.
Restore-sqmSysadminAccess automatisiert genau das (dbatools
Stop-DbaService/Set-DbaStartupParameter -SingleUser/Start-DbaService), fuehrt
Login-Anlage/-Reset + Rollenvergabe + Trigger-Deaktivierung/-Reaktivierung dabei in
EINEM einzigen T-SQL-Batch/EINER einzigen Verbindung aus (Single-User-Mode erlaubt nur
genau eine Verbindung - jeder zusaetzliche Roundtrip waere ein Zeitfenster fuer einen
konkurrierenden Verbindungsversuch) und baut den Single-User-Mode im finally-Block in
JEDEM Fall wieder zurueck, auch nach einem Fehler mitten in der Prozedur. Generiertes
Passwort wird nur im Rueckgabeobjekt ausgegeben, nie geloggt.
ConfirmImpact bleibt bewusst 'High' MIT aktivem interaktivem Rueckfrage-Dialog (anders
als z.B. Grant-sqmTemporarySysadmin) - gedacht als manuelle Notfallmassnahme am
Keyboard, nicht fuer unbeaufsichtigte Automation.
Funktionsanzahl im Modul: 163 -> 164.
Get-sqmDatabaseRestoreHistory - letzter Restore-Zeitpunkt je DatenbankBislang gab es im Modul keine Funktion, die auflistet, wann eine Datenbank zuletzt restauriert wurde - relevant fuer Compliance-Nachweise, Migrations-/Test-Nachverfolgung und um zu erkennen, ob eine erwartete Restore-Prozedur tatsaechlich gelaufen ist.
Get-sqmDatabaseRestoreHistory listet fuer jede Datenbank auf der Instanz (gleiche
Filterung wie Get-sqmDatabaseHealth: exkl. tempdb, System-DBs standardmaessig ausgeschlossen,
-ExcludeDatabase-Wildcards) den letzten Restore-Vorgang aus msdb.dbo.restorehistory (via
dbatools Get-DbaDbRestoreHistory -Last): Zeitpunkt, Typ (Database/Log/Differential/...),
durchfuehrender Login und Quelldatei. Datenbanken ohne jede Restore-Historie - der Normalfall bei
den meisten Produktivdatenbanken - werden explizit als "(nie restauriert)" ausgewiesen statt
stillschweigend aus der Liste zu fehlen (reines Get-DbaDbRestoreHistory liefert nur Zeilen fuer
tatsaechlich restaurierte Datenbanken).
Berichte werden wie bei den uebrigen Get-sqm*-Reportfunktionen als CSV/TXT/HTML unter
-OutputPath abgelegt (Default: <OutputPath config>\DatabaseRestoreHistory).
Unit-Tests (Pester, gemockt dbatools): 10/10 gruen. Gesamte Modul-Testsuite (229 Tests) gruen. Funktionsanzahl im Modul: 162 -> 163 (README aktualisiert).
Invoke-sqmRestoreDatabase scheiterte trotz erfolgreichem SetSingleUser mit "Exclusive access could not be obtained"Gemeldet anhand eines echten Laufs (SQL01, Restore von SalesDB aus
F:\Transfer\SalesDB.bak): SetSingleUser protokollierte Erfolg, der direkt
anschliessende Restore-DbaDatabase-Aufruf scheiterte trotzdem mit "Exclusive access could not
be obtained because the database is in use."
Ursache: ALTER DATABASE ... SET SINGLE_USER WITH ROLLBACK IMMEDIATE lief ueber
Invoke-DbaQuery, dessen Verbindung danach geschlossen wird - der einzige Verbindungs-Slot war
damit wieder frei. Bis Restore-DbaDatabase seine eigene, neue Verbindung aufbaute (dazwischen
liegt zusaetzlich noch RESTORE FILELISTONLY fuer das Auto-FileMapping), konnte sich eine fremde
Session neu verbinden und den Slot belegen - der eigentliche RESTORE-Befehl fand die Datenbank
dann wieder "in use" vor.
Fix: Der Restore-Schritt erkennt genau diese Fehlermeldung jetzt und beendet alle Sessions auf der
Zieldatenbank per Stop-DbaProcess, bevor er es erneut versucht (bis zu 3 Versuche). Ein erneutes
ALTER DATABASE SET SINGLE_USER haette hier NICHT geholfen - dieser Befehl braucht selbst
exklusiven Zugriff, der ja gerade durch die fremde Session blockiert ist (Henne-Ei-Problem);
Stop-DbaProcess wirkt dagegen unabhaengig vom aktuellen User-Access-Modus.
Beim Schreiben der Regressionstests dafuer zusaetzlich einen unabhaengigen, vorbestehenden Bug
gefunden und mitbehoben: -DatabaseName hatte neben dem an das FromHistory-Set gebundenen
Attribut ein zweites, unbenanntes [Parameter(Mandatory = $false)] (== __AllParameterSets).
Kombiniert mit den explizit benannten Sets SingleFile/Sequence (ueber -BackupFile/
-BackupFiles) konnte PowerShell den Parametersatz nicht mehr auflösen, sobald -BackupFile
UND -DatabaseName gemeinsam angegeben wurden - exakt der in .EXAMPLE dokumentierte
Standardfall. Der Aufruf scheiterte dadurch bereits beim Parameter-Binding mit
"Parameter set cannot be resolved", bevor auch nur eine Zeile der Funktion lief. Ein bestehender
Unit-Test hatte das unbemerkt ueberdeckt, weil er nur pauschal Should -Throw ohne
Meldungsabgleich pruefte - die eigentlich getestete Ursache (Get-sqmDatabaseAgMembership
schlaegt fehl) wurde dadurch nie erreicht. Fix: $DatabaseName bekommt jetzt fuer jedes Set ein
eigenes, explizit benanntes [Parameter(...)]-Attribut statt der unbenannten Variante.
Unit-Tests (Pester, gemockt): 17/17 gruen inkl. zweier neuer Regressionstests fuer den Exclusive-Access-Retry (erfolgreicher Retry nach 2 Fehlversuchen, endgueltiger Abbruch nach 3 Versuchen ohne Endlos-Retry) sowie einem verschaerften Test fuer den Parameterset-Fix. Gesamte Modul-Testsuite (219 Tests) weiterhin gruen.
Invoke-sqmSplunkConfiguration schrieb im Remote-/List-Modus kein Controller-LogGemeldet: "Ebenso soll ein Log als Standardverhalten geschrieben werden." Im Local-Modus schrieb
_sqmSplunk_LocalCore bereits immer ein vollstaendiges Log. Im Remote- (AD-OU) und List-Modus
ging jede Meldung des Controllers (AD-Suche, Verbindungsversuche pro Zielrechner, Abschluss-
Zusammenfassung) nur an Write-Host/GUI-Callback - ohne Log-Datei auf dem Rechner, von dem aus
der Befehl gestartet wurde. Nur die Zielrechner selbst schrieben (via Invoke-Command) ihr
eigenes lokales Log.
Fix: _sqmSplunkGuiLog schreibt jetzt immer zusaetzlich in eine Log-Datei, wenn eine angegeben
ist (Protokollierung ist Standardverhalten, kein Opt-in-Parameter noetig). Ein Log-Dateipfad wird
zentral einmal pro Aufruf berechnet und durchgereicht (_sqmSplunk_ForOU → _sqmSplunk_ForList →
_sqmSplunk_OnComputers), inklusive der Abschluss-Tabelle. _sqmSplunk_LocalCore nimmt jetzt
direkt einen fertigen Dateipfad entgegen statt selbst einen zu erzeugen - Zielrechner im
Remote-/List-Modus erzeugen ihren eigenen Dateinamen weiterhin selbst (auf sich selbst, getrennt
vom Controller-Log).
Live verifiziert (powershell.exe 5.1, DEV03): Local-, List- (inkl. nicht erreichbarem Host und
leerer Liste) und Remote-Modus erzeugen jeweils genau eine Log-Datei mit vollstaendigem
Nachrichtenverlauf; die zuvor behobene AmbiguousParameterSet-Regression (siehe [1.9.81.0])
bleibt bestehen.
Invoke-sqmSplunkConfiguration wirft ParameterBindingException beim Setzen von -Remote oder -SearchOUGemeldet: "Error beim ParameterBinding" beim Setzen von Remote oder SearchOU. Live nachgestellt
(powershell.exe 5.1): Sobald -ComputerList zusammen mit -Remote/-SearchOU gebunden wird -
auch mit leerem Array, $null oder Remote:$false reicht bereits die reine Angabe des Parameters -
wirft PowerShell AmbiguousParameterSet ("Der Parametersatz kann mit den angegebenen benannten
Parametern nicht aufgeloest werden."), noch bevor der Funktionsrumpf laeuft.
Ursache: Die param()-Deklaration hatte Remote/SearchOU und ComputerList ueber
ParameterSetName als hart gegenseitig exklusiv markiert - obwohl der Funktionsrumpf selbst
bereits eine Prioritaetskette dafuer implementiert (if ($Remote) ... elseif ($ComputerList) ...
else). Diese Rumpf-Logik war durch die strikte Set-Pruefung nie erreichbar, sobald beide Parameter
gleichzeitig gebunden wurden - z. B. wenn im GUI-Formular sowohl SearchOU befuellt als auch aus
einem vorherigen Versuch noch Text in ComputerList steht.
Fix: ParameterSetName-Attribute von Remote, SearchOU, ComputerList und Credential
entfernt. Die Funktion hat jetzt nur noch einen Parametersatz; welcher Ausfuehrungspfad
(Remote/List/Local) genutzt wird, entscheidet weiterhin die bereits vorhandene Prioritaetslogik im
Rumpf. Live verifiziert (powershell.exe 5.1, DEV03): alle zuvor fehlschlagenden Kombinationen
(SearchOU + leeres/$null/gesetztes ComputerList, Remote:$false + SearchOU, Remote:$true
+ leeres ComputerList) binden jetzt fehlerfrei und laufen im jeweils korrekten Zweig.
Get-sqmWaitStatistics und fünf Batch-B3-Geschwister schreiben jetzt auch ohne -OutputPath einen ReportGemeldet: "Get-sqmWaitStatistics erstellt keinen HTML-Report mehr". Live-Test gegen DEV01 mit
sowohl dem aktuellen Source-Stand als auch der auf einem Testrechner installierten Version
(v1.9.51.0) zeigte in beiden Staenden korrekten, funktionierenden CSV+HTML-Code - kein
Regressions-Bug. Ursache war Aufruf ohne -OutputPath: Anders als z. B. Get-sqmDiskSpaceReport
oder Get-sqmLoginSettings hatte Get-sqmWaitStatistics (und mit ihr vier weitere Funktionen aus
dem selben "Report-Standardisierung Batch B3", die im Juni zusammen die HTML-Ausgabe erhielten)
keinen Default-Wert fuer -OutputPath - ohne den Parameter wurde nie eine Datei geschrieben, auch
keine CSV, nur Objekte/Log.
Fix (Default-Wert nach dem bereits etablierten Muster (Join-Path (Get-sqmDefaultOutputPath)
'<Unterordner>'), wie es Get-sqmLoginSettings/Get-sqmDiskSpaceReport bereits nutzen):
- Get-sqmWaitStatistics → ...\WaitStatistics
- Get-sqmPerfCounters → ...\PerfCounters
- Get-sqmConnectionStats → ...\ConnectionStats
- Get-sqmMissingIndexes → ...\MissingIndexes
- Get-sqmLongRunningQueries → ...\LongRunningQueries
Get-sqmLoginSettings hatte bereits einen Default und war nicht betroffen.
Live gegen DEV01 verifiziert (frischer Modul-Reimport aus dem Source-Verzeichnis, nicht die
lokal installierte Kopie - siehe Hinweis in [[feedback_sqmsqltool_installed_vs_source]]):
Get-sqmWaitStatistics, Get-sqmConnectionStats und Get-sqmPerfCounters schreiben jetzt ohne
-OutputPath automatisch CSV+HTML in den Default-Ordner. Get-sqmMissingIndexes schreibt bei 0
Treffern weiterhin bewusst keine Datei (eigene, unveraenderte Bedingung -and $results.Count -gt
0) - kein Bug, sondern vermeidet leere Reports.
New-sqmAgentCommandJob, beliebige sqmSQLTool-Funktionen generisch als Agent-Job-StepHintergrund: Die bisherigen New-sqm*Job-Funktionen (New-sqmRestoreDatabaseJob,
New-sqmAlwaysOnRepairJob, ...) sind jeweils fest auf EINE Zielfunktion mit fest verdrahteten
Parametern zugeschnitten - jede baut ihre eigene Argument-String-Zeile per Hand. Fuer beliebige
sqmSQLTool-Funktionen mit unterschiedlichsten Parametersaetzen skaliert das nicht; ein frueherer
generischer Versuch (Private\New-sqmCmdExecJobStep.ps1 / _CreateCmdExecJobStep) baute
Parameter per doppelt-quotierter String-Interpolation in den generierten Wrapper ein und brach bei
$-Zeichen in Pfaden sowie durch hartkodierte, nicht auf jede Funktion zutreffende Flags
(-Verbose -ContinueOnError) - wird seither von den produktiven Job-Funktionen nicht mehr
verwendet (siehe deren Kommentare zu _q-Escaping).
New-sqmAgentCommandJob loest das strukturell statt durch besseres Escaping: Parameter werden nie
als Text in generierten PowerShell-Quellcode eingebettet, sondern per Export-Clixml typisiert in
eine Datei pro Step geschrieben. Ein einziger, wiederverwendbarer Wrapper (generic-invoke.ps1,
unter ...\sqmSQLTool\jobs\) laedt die Datei per Import-Clixml und ruft die per -FunctionName
uebergebene Zielfunktion per Splatting auf - dieselbe Wrapper-Datei bedient jede beliebige
sqmSQLTool-Funktion, ohne pro Funktion neuen Code zu generieren. -EnableException/-Confirm:$false
werden nur gesetzt, wenn Get-Command bestaetigt, dass die Zielfunktion den Parameter ueberhaupt
kennt - der Bug mit den hartkodierten Flags aus dem alten Ansatz kann so nicht mehr auftreten.
Get-Command -Module sqmSQLTool wirkt zusaetzlich als Allowlist (nur exportierte Funktionen sind
aufrufbar) und faengt Tippfehler im Funktionsnamen schon vor dem Anlegen ab.
Zwei Achsen sind unabhaengig waehlbar:
- Ein Job/mehrere Steps: -Command nimmt ein Array von Hashtables (FunctionName,
Parameters, optional StepName) - jeder Eintrag wird ein eigener, per GoToNextStep
verketteter CmdExec-Step; jeder Step beendet den Job bei Fehler sofort (QuitWithFailure,
Fail-Fast). -AppendStep haengt spaeter weitere Steps an einen bestehenden Job an; dabei wird
der bisher letzte Step automatisch von QuitWithSuccess auf GoToNextStep umgestellt (per
Set-DbaAgentJobStep -StepName - dieses Cmdlet hat kein -StepId, siehe Fix in [1.7.x] weiter
unten), sonst wuerden die neuen Steps nie erreicht.
- On-Demand/Scheduled: -ScheduleType None (Default) vs. Daily/Weekly/Monthly via
New-DbaAgentSchedule (gleiches Muster wie New-sqmRestoreTestJob), plus -StartJob fuer den
sofortigen ersten Lauf unabhaengig vom Schedule.
Sicherheitshinweis in den Function-Docs: keine PSCredential/SecureString-Werte in -Parameters
- Export-Clixml verschluesselt SecureStrings per DPAPI nur fuer das aktuelle Windows-Konto; laeuft
der Job unter einem anderen Konto (Dienstkonto/Proxy), schlaegt die Entschluesselung fehl. Fuer
erhoehte Rechte stattdessen einen dedizierten Agent-Proxy (New-sqmAgentProxy) verwenden.
Beim Live-Test per -WhatIf gegen DEV01 (workgroup, kein Domaenen-Trust) zwei Dinge gefunden und
korrigiert:
- Ein Bug in der Step-Schleife: die lokale Variable hiess $command - kollidiert case-insensitiv
mit dem typisierten Pflichtparameter $Command ([hashtable[]]). PowerShell erzwingt bei jeder
Zuweisung an eine parametertypisierte Variable weiterhin deren Typ; die Zuweisung des generierten
Kommando-Strings brach deshalb bei JEDEM echten Aufruf mit "Cannot convert ... to Hashtable[]" ab
- unabhaengig von -WhatIf. Fix: umbenannt zu $stepCommand.
- -SqlCredential ergaenzt (optional, wie bei New-sqmAgentProxy): ohne Domaenen-Trust zum
-SqlInstance schlagen die internen Get/New/Set-DbaAgentJob*-Aufrufe sonst mit Windows-Auth
fehl. Aendert nichts daran, wie der Job-Step selbst spaeter authentifiziert (siehe .NOTES) - gilt
nur fuer die Verbindung, mit der der Job ANGELEGT wird.
Beide Fixes und alle Pfade (Single-Step, Multi-Step-Kette mit Daily/Weekly-Schedule, -AppendStep
gegen einen echten vorhandenen Job inkl. automatischer GoToNextStep-Umstellung des bisher letzten
Steps, Allowlist-Ablehnung eines Tippfehler-Funktionsnamens) per -WhatIf gegen DEV01 verifiziert.
Echte Job-Anlage (ohne -WhatIf) erfordert eine elevated PowerShell-Session (Schreibzugriff auf
C:\Program Files\... - gilt fuer alle New-sqm*Job-Funktionen dieses Moduls) und wurde in dieser
Session mangels Elevation nicht ausgefuehrt.
Export-sqmDatabaseLogins / Import-sqmDatabaseLogins / Sync-sqmDatabaseLoginsHintergrund: Datenbanken wie TradingDB werden regelmaessig von Prod nach Test kopiert und dort
per Invoke-sqmRestoreDatabase restored. Die Datenbank enthaelt ca. 1500 SQL-Server-Logins als
Datenbank-User; die zugehoerigen Server-Logins mit den aktuellen Kennwoertern liegen nur auf Prod
und aendern sich dort von Zeit zu Zeit (Passwortrichtlinie/Ablauf) - Test bekommt das nie
mitgeteilt. Nach einem Refresh landen die Datenbank-User zwar korrekt (mit Prod-SIDs) in der
Test-DB, aber ein ggf. bereits vorhandenes gleichnamiges Test-Login hat eine andere SID und/oder
ein veraltetes Kennwort - Endanwender koennen sich mit ihrem aktuellen Prod-Kennwort nicht an Test
anmelden.
Prod und Test koennen dabei in unterschiedlichen, sich gegenseitig nicht sehenden Domaenen/Netzen
liegen (muss aber nicht so sein) - eine gleichzeitige Live-Verbindung von einer Kontrollmaschine zu
beiden Instanzen (wie es das bestehende Copy-sqmLogins voraussetzt) ist deshalb keine verlaessliche
Annahme. Die neue Loesung ist daher rein dateibasiert:
Export-sqmDatabaseLogins verbindet sich NUR zur Quelle (Prod), ermittelt die SQL-Auth-User
einer Datenbank, schliesst sysadmin-/sa-/System-Logins ohne Override-Moeglichkeit aus, und
schreibt pro Login einen eigenstaendigen, idempotenten T-SQL-Block (SID + Kennwort-Hash +
Policy-Flags) in ein menschenlesbares, auch manuell in SSMS ausfuehrbares Skript an einen
beliebigen Pfad. Jeder Block prueft zusaetzlich zur Laufzeit erneut, ob das Ziel-Login sysadmin/
sa ist, und laesst es dann unangetastet - ein zweites, unabhaengiges Sicherheitsnetz, das auch
bei versehentlicher Ausfuehrung gegen die falsche Instanz greift.Import-sqmDatabaseLogins verbindet sich NUR zum Ziel (Test), liest die von einem externen,
nicht von diesem Modul gesteuerten Kopiervorgang dorthin transportierte Datei, deaktiviert/
reaktiviert die konfigurierte PBM-Policy exakt wie Copy-sqmLogins, wendet jeden Login-Block
einzeln an (eigenes Status-Ergebnis pro Login: Success/SkippedSysadmin/
SkippedSidCollision/Failed) und ruft abschliessend Repair-DbaDbOrphanUser auf.Sync-sqmDatabaseLogins ist ein reiner Orchestrierungs-Wrapper fuer den Fall, dass eine
Kontrollmaschine tatsaechlich beide Instanzen erreicht: Export in eine Temp-Datei, sofort
Import, Temp-Datei loeschen - keine eigene Fachlogik, deckt beide Konnektivitaets-Faelle mit
denselben zwei Bausteinen ab.Invoke-sqmRestoreDatabase selbst wurde NICHT veraendert (kein zusaetzlicher Parameterpfad in einer
bereits sehr umfangreichen, AG-sensiblen Funktion) - fuer Datenbanken mit eigenstaendigen SQL-Logins
wie TradingDB wird Sync-sqmDatabaseLogins (oder das Export-/Import-Paar) direkt im Anschluss an
den Restore aufgerufen, siehe Verweis in dessen .NOTES.
Invoke-sqmPerfBaseline erfasst jetzt direkt CPU% und Memory (MB)Anwenderwunsch: neben Wait Stats und Perf-Counter-Deltas sollen CPU und Memory auch direkt
ausgewertet werden. Jeder Snapshot (-Action Capture) enthaelt jetzt zusaetzlich eine
Momentaufnahme aus sys.dm_os_ring_buffers (CPU%) und sys.dm_os_process_memory /
sys.dm_os_sys_memory (SQL-, Available- und Total-Memory in MB), gespeichert als
DirectMetrics-Feld im Baseline-JSON. Da es sich - anders als Wait Stats/Perf Counter - um
Momentwerte statt kumulative Zaehler handelt, zeigt -Action Compare sie nicht als Delta,
sondern als neue Sektion "CPU & Memory (Zeitpunkt der Erfassung)" mit A/B nebeneinander (HTML-
Report + Rueckgabeobjekt .DirectMetrics). Baseline-Dateien von vor diesem Feature (kein
DirectMetrics-Feld) werden erkannt und im Report als "Nicht verfuegbar" markiert statt einen
Fehler zu werfen.
Die dafuer noetigen DMV-Queries (CPU-Ring-Buffer, Memory) gab es in Get-sqmServerUtilization
bereits einzeln; beide wurden in den neuen privaten Helper Get-sqmDirectCpuMemory (ein
DMV-Call via OUTER APPLY statt zwei getrennte Queries) ausgelagert und Get-sqmServerUtilization
darauf umgestellt, um die Query-Logik nicht zu duplizieren.
Live gegen Docker-Testcontainer (mssql-2022-test) verifiziert: Capture, Compare (inkl.
neuer HTML-Sektion), sowie der Ruecksprung auf einen synthetischen Alt-Snapshot ohne
DirectMetrics. Dabei zeigte sich ein Bug in der ersten Implementierung: CROSS APPLY verhaelt
sich wie ein Inner Join, sodass bei leerem Ring-Buffer (z. B. kurz nach Instanz-Neustart, wie im
frisch gestarteten Testcontainer) die komplette Ergebniszeile inkl. Memory-Werten wegfiel - mit
OUTER APPLY behoben, CPU% faellt in diesem Fall korrekt auf 0 zurueck statt Memory mit
wegzureissen.
Invoke-sqmRestoreDatabase scheiterte beim AG-Rejoin mit "RecoveryModel ... is not Full, but Simple"Vorfall auf SQL01: Restore von Test aus einem Backup, das (oder dessen Quelldatenbank)
in SIMPLE Recovery stand. Restore, User-Import und Owner-Zuweisung liefen erfolgreich durch, aber
Add-DbaAgDatabase scheiterte zuverlaessig mit "RecoveryModel of database [Test] is not Full, but
Simple" - eine AG verlangt zwingend Full Recovery mit luecklosser Log-Chain.
Vor dem eigentlichen Rejoin-Versuch wird das Recovery Model jetzt geprueft; steht es nicht auf
FULL, stellt die Funktion automatisch um (ALTER DATABASE ... SET RECOVERY FULL) und erstellt
sofort danach ein FULL-Backup - SET RECOVERY FULL allein reicht nicht, bis zum naechsten
FULL-Backup verhaelt sich die Datenbank weiterhin wie SIMPLE ("pseudo-simple"), das haette
Add-DbaAgDatabase also unveraendert scheitern lassen. Neuer Ergebniseintrag Action =
EnsureFullRecoveryModel zeigt, ob/dass umgestellt wurde.
Invoke-sqmRestoreDatabase findet das Backup automatisch, wenn nur -DatabaseName angegeben wirdBisher musste immer -BackupFile oder -BackupFiles angegeben werden. Neuer Parameter-Set
FromHistory: wird WEDER -BackupFile NOCH -BackupFiles angegeben, wird -DatabaseName
zwingend erforderlich, und die aktuellste Wiederherstellungskette (neuestes Full plus alle
seitherigen Diff/Log-Backups) wird automatisch aus der Backup-Historie der Instanz ermittelt
(Get-DbaDbBackupHistory -Database <name> -Last, d.h. msdb.dbo.backupset) und zum
spaetestmoeglichen Zeitpunkt wiederhergestellt - der Aufrufer muss keinen Dateipfad mehr kennen:
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -DatabaseName "AdventureWorks"
Gegen DEV01 verifiziert (-WhatIf): die Funktion findet und verwendet korrekt die tatsaechlich
neueste Full-Backup-Datei aus der Historie, ohne dass ein Pfad angegeben wurde.
Invoke-sqmUserDatabaseBackup verlangte bislang, dass der Backup-Pfad auf "User-Db" endet - eine
andere Schreibweise als die im Rest des Moduls tatsaechlich verwendete und angelegte "Usr-db"
(siehe New-sqmOlaUsrDbBackupJob: legt real <BackupDirectory>\Usr-db an). Ein voellig
regelkonformer, von den eigenen Backup-Job-Funktionen selbst angelegter Pfad waere von
Invoke-sqmUserDatabaseBackup also faelschlich abgelehnt worden. Dieselbe falsche Annahme
("User-db" statt "Usr-db") steckte auch in Invoke-sqmTsmConfigurations Standard-INCLUDE-Regel -
das TSM-INCLUDE zeigte auf einen Ordner, den keine der Backup-Job-Funktionen je befuellt, das
tatsaechliche "Usr-db"-Verzeichnis waere von TSM nie gesichert worden. Beide jetzt auf "Usr-db"
korrigiert.
Show-sqmToolGui markierte Alternativ-Parameter (z.B. BackupFile/BackupFiles) faelschlich als PflichtfeldDie Label-Anzeige im GUI-Launcher markierte ein Parameterfeld mit einem fett gedruckten ""
(Pflichtfeld), sobald IRGENDEINE seiner ParameterAttribute-Auspraegungen Mandatory = $true
gesetzt hatte - unabhaengig vom Parameter-Set. Bei Invoke-sqmRestoreDatabase ist -BackupFile
nur im Set SingleFile, -BackupFiles nur im Set Sequence Mandatory - beides sind
Alternativen, keine zwei gleichzeitig auszufuellenden Pflichtfelder. Das GUI zeigte deshalb bei
BEIDEN ein "", obwohl nur eines gebraucht wird - genau dieselbe Fehlerklasse, die fuer die
Run-Button-Validierung bereits in 1.9.43.0 behoben wurde, hier aber nie nachgezogen.
Fix: Die Mandatory-Pruefung fuer das Label vergleicht jetzt wie die Run-Validierung ueber
Command.ParameterSets - "*" (fett) erscheint nur noch, wenn ein Parameter in JEDEM
Parameter-Set der Funktion Pflicht ist (also unbedingt noetig, egal welches Set gewaehlt wird).
Parameter, die nur in EINIGEN Sets Pflicht sind (Alternativen wie BackupFile/BackupFiles),
bekommen stattdessen ein leichteres "+"-Zeichen; der Tooltip nennt das/die betroffene(n)
Parameter-Set(s). Betrifft neben Invoke-sqmRestoreDatabase auch Get-sqmDiskBlockSize,
Get-sqmServerSetting und New-sqmRestoreDatabaseJob, die dasselbe Alternativ-Muster haben.
New-sqmBackupMaintenanceJob liest den Jobnamen jetzt aus der Konfiguration-JobName hatte bisher den fest verdrahteten Default 'sqm-BackupMaintenance-FULL' -
unabhaengig vom gewaehlten -BackupType. Ein Aufruf mit -BackupType DIFF ohne -JobName legte
also einen SQL-Agent-Job an, der sqm-BackupMaintenance-FULL hiess, obwohl er tatsaechlich ein
DIFF-Job war.
-JobName hat jetzt keinen festen Default mehr; ohne explizite Angabe wird der Name aus der
Konfiguration gelesen, abhaengig vom gewaehlten -BackupType
(Set-sqmConfig -BackupMaintenanceJobNameFull/-Diff/-Log), analog zum bereits bestehenden Muster
bei New-sqmOlaUsrDbBackupJob (OlaJobNameFull/Diff/Log). Ist auch das nicht konfiguriert, greift
weiterhin ein typspezifischer Default (sqm-BackupMaintenance-<BackupType>), diesmal korrekt
abhaengig vom Typ statt immer "FULL".
Im GUI-Launcher (Show-sqmToolGui) fuellt eine Auswahl in der BackupType-Dropdown-Liste (FULL/
DIFF/LOG) das JobName-Feld jetzt automatisch mit dem konfigurierten bzw. Default-Namen - der
Anwender muss ihn im Regelfall nicht mehr selbst eintippen, kann ihn danach aber weiterhin frei
ueberschreiben.
Alle drei Aenderungen gegen DEV01 verifiziert (Jobname-Aufloesung inkl. mit und ohne konfiguriertem Wert; DEV01 selbst war waehrend eines Teils der Tests wegen eines offensichtlich voruebergehenden DNS-Ausfalls im Labor nicht erreichbar - unabhaengig von dieser Aenderung, die Namensaufloesung selbst wurde unabhaengig davon per direktem Objektvergleich bestaetigt).
Die zurueckgegebenen Objekte enthalten DefaultDatabase und DefaultLanguage (die CSV hatte sie
deshalb schon immer, da sie die vollen Objekte exportiert) - TXT und HTML liessen beide Felder
aber aus. Beide Spalten jetzt in TXT (unter jedem Login) und HTML (eigene Tabellenspalten,
HTML-encodiert) ergaenzt. Gegen DEV01 verifiziert.
Get-sqmAlwaysOnFailoverHistory, Get-sqmTlsStatus und Invoke-sqmInstanceInventory schrieben
bislang nur TXT + CSV, obwohl sie wie die anderen "voll ausgebauten" Reports (z.B.
Get-sqmSysadminAccounts) laengst einen sinnvoll farbcodierbaren Status pro Zeile berechnen. Alle
drei bekommen jetzt zusaetzlich einen HTML-Report im etablierten Stil (ConvertTo-sqmHtmlReport):
Get-sqmAlwaysOnFailoverHistory: FailoverType farblich markiert (Forced=rot, Automatic=gelb,
Planned=gruen).Get-sqmTlsStatus: Status farblich markiert (Critical=rot, Warning=gelb, OK=gruen), Ergebnisse
nach Schweregrad sortiert.Invoke-sqmInstanceInventory: alle TXT-Abschnitte (Instanz, Datenbanken, Logins, Linked Server,
Agent Jobs, Konfigurationsabweichungen, Always On) als HTML-Tabellen; Datenbanken ohne
Vollsicherung und sysadmin-Logins gelb markiert, fehlgeschlagene Agent-Jobs rot. Bekam dabei
auch den bislang fehlenden -NoOpen-Switch und das automatische Oeffnen des Reports, analog zu
den anderen Funktionen.Alle drei zusaetzlich gegen DEV01 verifiziert (Instance Inventory und TLS-Status remote, Failover- Historie lokal - RPC-Zugriff auf das entfernte Application-Event-Log ist in diesem Workgroup-Labor ohne Domaene nicht verfuegbar, unabhaengig von dieser Aenderung).
C:\System\WinSrvLog\MSSQL (der Standard-Ausgabepfad) sammelte die TXT/CSV/HTML-Dateien von
mittlerweile ueber 40 verschiedenen Funktionen gemischt im selben flachen Ordner - die Anzahl der
Dateien darin wuchs entsprechend schnell. Get-sqmServerHardwareReport machte es von Anfang an
richtig (eigenes Unterverzeichnis HardwareReports); alle anderen schrieben direkt in die Wurzel.
33 weitere Funktionen (siehe Liste unten) bekommen jetzt jeweils ihr eigenes Unterverzeichnis nach
demselben Muster, z.B. Get-sqmSysadminAccounts -> SysadminAccounts, Get-sqmDatabaseHealth ->
DatabaseHealth. Die drei Listener-Migrationsschritte (Prepare-sqmListenerForMigration,
Move-sqmAlwaysOnListener, Complete-sqmListenerMigration) teilen sich bewusst EIN Verzeichnis
(ListenerMigration), da sie Phasen desselben Vorgangs sind. Reine Pfad-Aenderung an den jeweiligen
-OutputPath-Standardwerten, keine Aenderung an der Report-Erstellung selbst. Bereits vorhandene
Dateien im Wurzelordner bleiben unangetastet liegen - nur neue Laeufe landen im Unterordner.
Nebenbei vereinheitlicht: mehrere Funktionen nutzten bislang einen hartcodierten Pfad-String statt
der zentralen Get-sqmDefaultOutputPath, wodurch eine ueber Set-sqmConfig gesetzte abweichende
OutputPath-Konfiguration fuer sie ignoriert wurde. Jetzt einheitlich ueber die zentrale Funktion.
Bugfix dabei gefunden: Get-sqmServerUtilization legte sein Ausgabeverzeichnis nie selbst an -
das fiel nie auf, solange $OutputPath auf die (durch andere Funktionen laengst vorhandene) flache
Wurzel zeigte. Mit eigenem Unterverzeichnis schlug der erste Lauf mit "Could not find a part of the
path" fehl. Verzeichniserstellung ergaenzt, gegen DEV01 verifiziert.
Betroffene Funktionen (neues Unterverzeichnis in Klammern): Get-sqmSpnReport (SpnReports), Get-sqmCertificateReport (CertificateReports), Get-sqmAgentJobScheduleReport (AgentJobSchedule), Get-sqmAlwaysOnHealthReport (AlwaysOnHealth), Get-sqmDiskSpaceReport (DiskSpaceReports), Compare-sqmServerConfiguration (ServerConfigCompare), Get-sqmADMemberGroups (ADMemberGroups), Get-sqmDatabaseHealth (DatabaseHealth), Compare-sqmAlwaysOnRoles (AlwaysOnRoleCompare), Get-sqmADGroupMembersRecursive + Get-sqmADGroupMembers (ADGroupMembers, geteilt - rekursive und nicht-rekursive Variante desselben Reports), Get-sqmServerUtilization (ServerUtilization), Compare-sqmAlwaysOnLogins (AlwaysOnLoginCompare), Get-sqmSysadminAccounts (SysadminAccounts), Export-sqmAlwaysOnConfiguration (AlwaysOnConfiguration), Get-sqmDistributedAgHealth (DistributedAgHealth), Export-sqmDatabaseDocumentation (DatabaseDocumentation), Get-sqmServiceBrokerHealth (ServiceBrokerHealth), Invoke-sqmLoginAudit (LoginAudit), Invoke-sqmSetupReport (SetupReports), Set-sqmDatabaseOwner (DatabaseOwnerChanges), Enable-sqmServiceBroker (ServiceBroker), Invoke-sqmServiceBrokerAlwaysOn (ServiceBrokerAlwaysOn), Invoke-sqmDistributedFailover (DistributedFailover), New-sqmDistributedAvailabilityGroup (DistributedAG), Prepare-sqmListenerForMigration + Move-sqmAlwaysOnListener + Complete-sqmListenerMigration (ListenerMigration, geteilt), Invoke-sqmAlwaysOnSetup (AlwaysOnSetup), Invoke-sqmCollationChange (CollationChange), Invoke-sqmTsmConfiguration (TsmConfiguration), Invoke-sqmSsisConfiguration (SsisConfiguration), Set-sqmSsrsConfiguration (SsrsConfiguration), Install-sqmCertificate (CertInstall).
Bisher gab Get-sqmLoginSettings nur Objekte zurueck; ein CSV+HTML-Export existierte, war aber
rein opt-in (nur bei explizit angegebenem -OutputPath), erzeugte keine TXT-Datei, und das HTML
war eine generische ConvertTo-Html -Fragment-Tabelle ohne jede farbliche Kennzeichnung - obwohl
die Funktion pro Login laengst ein RiskLevel/RiskIcon berechnet (Critical/Warning/OK/N-A).
-OutputPath hat jetzt einen Standardwert (<Modul-Standardpfad>\LoginSettings - eigenes
Unterverzeichnis, analog zu Get-sqmServerHardwareReport\HardwareReports) und der Bericht wird
bei jedem Aufruf automatisch geschrieben:
LoginSettings_<instanzen><datum>.html - nach RiskLevel farblich markiert (rot/gelb/gruen)
LoginSettings<instanzen><datum>.txt - gruppiert nach Critical/Warning
LoginSettings<instanzen>_<datum>.csv - maschinenlesbar
Rueckgabewert (flache Login-Objektliste) bleibt unveraendert - bestehende Aufrufe/Pipelines
brechen nicht.
Gegen DEV01 bestaetigt: HTML korrekt nach Risiko sortiert und eingefaerbt (Critical/Warning rot/ gelb, N/A ungefaerbt), TXT korrekt gruppiert, CSV vollstaendig.
Die Datenbank-Tabelle in Invoke-sqmSetupReport las $db.LastFullBackupDate - diese Property
existiert auf dem SMO/dbatools-Datenbankobjekt gar nicht (richtiger Name: LastBackupDate). Der
Tippfehler warf keinen Fehler, PowerShell liefert bei einer nicht existierenden Property auf
diesem Objekttyp stillschweigend $null zurueck - $lastBackup war deshalb fuer JEDE Datenbank
auf JEDER Instanz immer $null, und die Spalte "Letzte Vollsicherung" zeigte ausnahmslos "Never",
auch wenn tatsaechlich taeglich gesichert wurde.
Zusaetzlich liefert SMO fuer eine Datenbank OHNE jede Vollsicherung nicht $null, sondern
DateTime.MinValue (01.01.0001) - ein "wahrer" Wert in PowerShell. Die reine Umbenennung auf
LastBackupDate haette fuer echte "nie gesicherte" Datenbanken deshalb eine astronomische
Tageszahl statt "Never" erzeugt; eine zusaetzliche Year -gt 1900-Prufung faengt das ab.
Gegen DEV01 bestaetigt: Datenbanken mit vorhandener Vollsicherung zeigen jetzt den echten Tagesabstand ("4 days" statt immer "Never"), echte Nie-gesichert-Faelle zeigen weiterhin korrekt "Never".
Folgefix zu 1.9.68.0: dieselbe Ursache (geteiltes Dienstkonto ueber alle AG-Repliken +
Listener), aber fuer die Partnerreplik selbst statt nur fuer den Listener. Der generische
Soll-/Ist-Vergleich kennt nur die 4 eigenen Instanz-SPNs - SPNs, die im setspn -L-Ergebnis
auftauchen, aber zu einer anderen Replik derselben AG gehoeren (z.B. SQL02 im Bericht
von SQL01), wurden faelschlich als Unexpected gemeldet, obwohl das im AG-Kontext mit
geteiltem Dienstkonto normal und korrekt ist.
Get-sqmSpnReport ermittelt jetzt zusaetzlich ueber sys.availability_replicas die
Partnerrepliken der lokalen AG(s) und erkennt SPNs, deren Host-Anteil zu einer Partnerreplik
gehoert, als OK mit erklaerendem Hinweis ("AlwaysOn Partner-SPN (Replik: ..., gemeinsames
Dienstkonto) - kein Handlungsbedarf."). Die dabei entstehende veraltete generische
Unexpected-Zeile fuer dieselbe SPN wird durch dieselbe Bereinigung wie in 1.9.68.0 entfernt
(jetzt erweitert um das Partner-SPN-Notat). Echte, nicht einer bekannten Replik oder dem Listener
zuordenbare Fremd-SPNs werden weiterhin als Unexpected gemeldet.
Bei einer AlwaysOn-Instanz, deren Repliken und Listener dasselbe Dienstkonto nutzen (Kerberos
zum Listener setzt das praktisch voraus), liefert setspn -L fuer dieses eine Konto auch die
SPNs der anderen Repliken und des Listeners zurueck. Der generische Soll-/Ist-Vergleich kennt
aber nur die 4 eigenen Instanz-SPNs und markierte alles andere - inklusive der Listener-SPNs -
pauschal als Unexpected. Der separate, spaeter laufende AG-Listener-Check bewertete dieselben
SPNs anschliessend korrekt gegen die Listener-Erwartungsliste (OK/Missing), fuegte dafuer aber
nur eine ZUSAETZLICHE Zeile hinzu statt die aeltere zu ersetzen - dieselbe SPN stand danach
zweimal im Bericht, einmal [Unexpected] und einmal [OK].
Beispiel (2-Knoten-AG, Listener AG01, geteiltes Dienstkonto):
MSSQLSvc/AG01:1433 [Unexpected] <- generischer Vergleich, kennt Listener-SPNs nicht
MSSQLSvc/AG01:1433 [OK] <- Listener-Check, korrekt
Nach dem Listener-Check wird jetzt die veraltete generische Unexpected-Zeile fuer jede SPN
entfernt, die der Listener-Check bereits eigenstaendig bewertet hat - die praezisere
Listener-Bewertung bleibt als einzige Zeile stehen. Echte Fremd-SPNs (z.B. der anderen AG-Replik)
werden weiterhin unveraendert als Unexpected gemeldet.
Im Kunden-Zweig von Install.ps1 (Quelle <Freigabe>\... bzw. \\<Freigabe>\...) lief robocopy ... /MIR
fuer dbatools und dbatools.library bislang bedingungslos bei jeder Installation - anders als
der PSGallery-Zweig gab es keinen "ist eh schon aktuell"-Kurzschluss. /MIR muss dafuer den
kompletten Dateibaum auf beiden Seiten enumerieren und vergleichen (dbatools: mehrere tausend
kleine .ps1-Dateien), ueber die Citrix-Freigabe \\<Freigabe>\ mit spuerbarer Latenz pro Datei -
das dauerte lange, selbst wenn am Ende nichts zu kopieren war.
Jetzt wird vorab die hoechste Versions-Ordnernummer auf Quelle und Ziel verglichen
(Get-sqmModuleFolderVersion); stimmen sie ueberein, wird robocopy fuer das jeweilige Modul
komplett uebersprungen. Muss synchronisiert werden (fehlende/veraltete Installation), laeuft
robocopy zusaetzlich mit /MT:8 (Multithread) statt seriell.
sys.dm_server_services listet nur den SQL Server-, Agent- und ggf. Full-Text-Dienst DER
VERBUNDENEN INSTANZ - der SQL Server Browser ist maschinenweit und taucht dort NIE auf. Die in
1.9.65.0 eingefuehrte Abfrage (servicename LIKE '%Browser%') lieferte deshalb immer 0 Zeilen
und meldete den Browser dauerhaft als "OK (nicht installiert)", unabhaengig vom tatsaechlichen
Zustand - auch wenn er lief oder nur gestoppt/deaktiviert war.
Gegen DEV01 bestaetigt: sys.dm_server_services zeigt dort ausschliesslich Engine + Agent, kein
Browser-Eintrag, obwohl der Dienst laeuft (Start: Automatic).
Ersetzt durch xp_servicecontrol N'QUERYSTATE', N'SQLBROWSER' fuer den Laufzustand und
xp_regread gegen HKLM\SYSTEM\CurrentControlSet\Services\SQLBrowser\Start fuer den Starttyp -
beides weiterhin ohne WinRM, ueber die bestehende SQL-Verbindung.
Invoke-sqmSetupReport prueft jetzt zusaetzlich den Status des SQL Server Browser-Dienstes
(sys.dm_server_services, servicename LIKE '%Browser%') und weist ihn im Abschnitt
"Server-Level Features" aus. Laeuft der Dienst, erscheint eine Warnung (orange): der Browser
ermoeglicht per UDP 1434 das Auffinden benannter Instanzen und ihrer dynamischen Ports im
Netzwerk und ist nur fuer genau diesen Fall noetig - laeuft er unnoetig mit, ist das ein
zusaetzlicher Angriffsvektor zur Instanz-Enumeration. Ist er gestoppt oder nicht installiert,
erscheint OK (gruen).
Die dbatools-PBM-Cmdlets setzen den SMO-PolicyStore voraus und brechen unter PowerShell 7 mit
"Get-DbaPbmStore: This command is not supported on Linux or macOS" ab. Zusammen mit
-ErrorAction SilentlyContinue bzw. der -ContinueOnError-Auswertung fuehrte das zu einem
lautlosen Fehlschlag: die Policy wurde nie deaktiviert, der Aufrufer lief ungeschuetzt weiter.
Gegen DEV01 unter beiden Versionen gemessen (identisches Skript, identische Instanz):
| PowerShell 5.1 | PowerShell 7 | |
|---|---|---|
| Policy nach Disable | is_enabled=0 |
unveraendert 1 |
syspolicy_server_trigger |
von PBM entfernt | bleibt bestehen |
CREATE LOGIN ... FROM WINDOWS |
erfolgreich | blockiert |
Bemerkenswert dabei (und ebenfalls verifiziert): SQL Server entfernt den
syspolicy_server_trigger automatisch, sobald keine aktivierte "On Change: Prevent"-Policy mehr
existiert, und legt ihn beim Aktivieren neu an - die Trigger-Existenz wird von PBM selbst
verwaltet.
Fix: Set-sqmSqlPolicyState nutzt jetzt ausschliesslich T-SQL (msdb.dbo.syspolicy_policies zum
Pruefen, msdb.dbo.sp_syspolicy_update_policy zum Umschalten) statt der dbatools-PBM-Cmdlets,
inklusive Verifikation des Ergebnisses statt "keine Exception = Erfolg". Verhaelt sich unter
PS 5.1 und PS 7 identisch - beide Wege gegen DEV01 nachgewiesen.
Vollstaendiges Audit des Moduls auf weitere Betroffene: dbatools 2.8.2 sperrt genau 6 Befehle
ausserhalb der Windows PowerShell (Copy-DbaSsisCatalog, Export-DbaCredential,
Export-DbaLinkedServer, Get-DbaPbmStore, Get-DbaSsisEnvironmentVariable,
New-DbaSsisCatalog). Davon nutzt sqmSQLTool nur den PBM-Pfad, an genau zwei Stellen:
Set-sqmSqlPolicyState (oben) und Invoke-sqmRestoreDatabase (Policy-Deaktivierung vor dem
Restore, dort ebenfalls mit -ErrorAction SilentlyContinue und damit lautlos) - beide auf die
Katalogsicht umgestellt. Die uebrigen vier Befehle kommen im Modul nicht vor.
Auf SQL01 stellte sich heraus, dass die betroffenen Logins die sysadmin-Rolle bereits
ueber AD-Gruppenmitgliedschaft besassen. Das hat eine Konsequenz, die ueber den Bug aus 1.9.61.0
hinausgeht und die Kernzusage dieser Funktion beruehrt: ALTER SERVER ROLE ... DROP MEMBER
entfernt beim Entzug nur die DIREKTE Mitgliedschaft. Eine ueber eine Gruppe geerbte Berechtigung
bleibt danach unveraendert bestehen - die zeitliche Befristung waere also nur scheinbar gegeben,
und niemand haette es bemerkt, weil das Tool sauberen Erfolg meldete.
Neu in Invoke-sqmTempSysadminAction:
- Beim Grant wird vorab geprueft, ob die Rolle bereits ueber einen anderen Pfad wirksam ist
(effektiv ja, direkt nein). Ist das so, wird die zusaetzliche direkte Mitgliedschaft zwar
vergeben (sie schafft einen sauberen, befristeten Audit-Eintrag), aber mit einer WARNUNG in
Logfile und Eventlog (EventId 9011) versehen, dass die geerbte Berechtigung davon unberuehrt
bleibt.
- Beim Revoke wird NACH dem Entzug geprueft, ob der Login weiterhin effektiv Mitglied ist. Falls
ja, warnt das Tool ausdruecklich (Logfile + Eventlog, EventId 9012), dass die privilegierten
Rechte NICHT beendet wurden und dafuer die Gruppenmitgliedschaft angepasst werden muss.
Bewusst als Warnung und nicht als Abbruch umgesetzt: die zusaetzliche direkte Mitgliedschaft ist fuer die Nachvollziehbarkeit (wer war wann per Ticket erhoeht) durchaus sinnvoll - sie darf nur nicht mit einer echten Befristung verwechselt werden.
Verifiziert gegen DEV01, dass der Normalfall (keine geerbten Rechte) unveraendert und ohne
Fehlalarm durchlaeuft. Der geerbte Fall selbst liess sich in der Testumgebung nicht nachstellen
(Workgroup, keine Domaene, kein BUILTIN\Administrators-Login) - die Einzelbausteine der
Erkennung (IS_SRVROLEMEMBER() effektiv, Katalogsicht direkt) sind aber jeweils belegt.
Get-sqmLoginSettings untertrieb sysadmin-Privilegien, die ueber AD-Gruppen geerbt sindDirekte Folge der Erkenntnis aus 1.9.61.0: die IsSysAdmin-Spalte pruefte ausschliesslich die
direkte Mitgliedschaft (sys.server_role_members). Ein Login, das nur ueber eine Windows-/
AD-Gruppe sysadmin ist, taucht dort gar nicht auf - der Sicherheitsreport wies ihn also als
harmlos aus, obwohl er dieselben Vollrechte hat. Fuer einen Report, dessen Zweck das Aufspueren
privilegierter Konten ist, ist das die gefaehrlichere Fehlrichtung.
Umgestellt auf "privilegiert ueber IRGENDEINEN Pfad" - bewusst die Kombination beider Quellen
statt eines simplen Austauschs, weil beide je nach Richtung untertreiben:
IS_SRVROLEMEMBER() erfasst die Vererbung ueber Gruppen, liefert aber seinerseits 0 fuer
manche direkten Mitglieder (auf DEV01 mit NT Service\MSSQLSERVER reproduziert);
sys.server_role_members erfasst nur direkte Mitgliedschaft. IsSysAdmin ist jetzt wahr, wenn
eine der beiden Quellen anschlaegt (ISNULL()-gesichert, damit nicht aufloesbare Logins - z.B.
verwaiste AD-Konten, IS_SRVROLEMEMBER() liefert dann NULL - nicht faelschlich als privilegiert
gelten).
Neue Spalte IsSysAdminDirect weist die direkte Mitgliedschaft weiterhin separat aus. Weichen
beide ab (IsSysAdmin=True, IsSysAdminDirect=False), sind die Rechte ueber eine Gruppe geerbt
und lassen sich nicht per ALTER SERVER ROLE ... DROP MEMBER am Login selbst entziehen, sondern
nur ueber die Gruppenmitgliedschaft - eine betrieblich wichtige Unterscheidung. RiskLevel
bewertet weiterhin auf Basis von IsSysAdmin, jetzt also inklusive geerbter Privilegien.
Verifiziert gegen DEV01: NT Service\MSSQLSERVER wird korrekt als IsSysAdmin=True erkannt,
obwohl IS_SRVROLEMEMBER() dafuer 0 liefert. Der umgekehrte Fall (nur ueber AD-Gruppe geerbt)
liess sich mangels Domaene in der Testumgebung nicht live nachstellen.
IS_SRVROLEMEMBER() prueft EFFEKTIVE, nicht direkte RollenmitgliedschaftDas ist die eigentliche Ursache hinter "kein Fehler, Log sagt erfolgreich, trotzdem kein sysadmin" auf SQL01 - und auch hinter allen vorherigen Fehlversuchen in dieser Reihe.
IS_SRVROLEMEMBER('sysadmin', N'<Login>') liefert die EFFEKTIVE Mitgliedschaft inklusive
Vererbung ueber Windows-/AD-Gruppen. Ist der Login Mitglied einer AD-Gruppe, die ihrerseits als
sysadmin-Login auf der Instanz eingetragen ist, liefert die Funktion bereits 1, obwohl der Login
selbst NIE direkt in die Serverrolle aufgenommen wurde. Die bedingte Anweisung
IF IS_SRVROLEMEMBER(...) = 0 ALTER SERVER ROLE ... ADD MEMBER wurde dadurch uebersprungen -
ohne Fehler, ohne Effekt. Die in 1.9.60.0 ergaenzte Verifikation nutzte dieselbe Funktion und
bestaetigte den Trugschluss deshalb nur (SELECT IS_SRVROLEMEMBER(...) = 1, obwohl keine direkte
Mitgliedschaft bestand).
Die beiden Semantiken weichen nachweislich voneinander ab - gegen DEV01 in BEIDE Richtungen
reproduziert: NT Service\MSSQLSERVER ist direktes Mitglied von sysadmin
(sys.server_role_members), waehrend IS_SRVROLEMEMBER() dafuer 0 liefert.
Fix: sowohl die Bedingung als auch die Verifikation in Invoke-sqmTempSysadminAction pruefen
jetzt die DIREKTE Mitgliedschaft ueber sys.server_role_members (JOIN auf
sys.server_principals) statt IS_SRVROLEMEMBER(). Fuer "diesen Login temporaer in die Rolle
aufnehmen bzw. daraus entfernen" ist ausschliesslich die direkte Mitgliedschaft massgeblich -
eine ueber eine AD-Gruppe geerbte Berechtigung liesse sich per DROP MEMBER ohnehin nicht
entziehen. Als Nebeneffekt entfaellt die NULL-Sonderbehandlung: die neue Bedingung liefert immer
0 oder 1, nie NULL.
Verifiziert gegen DEV01: Grant -> direkte Mitgliedschaft 1, erneuter Grant (idempotent) erfolgreich, Revoke -> 0, erneuter Revoke (idempotent) erfolgreich.
Invoke-sqmTempSysadminAction meldete Erfolg, obwohl die Rolle nie vergeben wurdeAuf SQL01 beobachtet: kein Fehler, der Revoke-Job wird angelegt, aber die Rolle war
danach nachweislich nicht vergeben. Ursache: die Grant-/Revoke-Anweisung ist bedingt (IF
IS_SRVROLEMEMBER('$Role', N'$Login') = 0 ALTER SERVER ROLE ... ADD MEMBER ...) -
IS_SRVROLEMEMBER() liefert laut Doku NULL statt 0/1, wenn es Login oder Rolle nicht
eindeutig aufloesen kann (gegen DEV01 bestaetigt: liefert NULL fuer einen nicht auflösbaren
Login). NULL = 0 ist in T-SQL weder wahr noch falsch, das IF wird dann STILLSCHWEIGEND
uebersprungen, ohne dass ALTER SERVER ROLE je laeuft und ohne dass eine Exception entsteht -
Invoke-DbaQuery -EnableException wirft nur bei echten SQL-Fehlern, ein uebersprungenes IF ist
kein Fehler. Der Code vertraute bisher ausschliesslich auf "keine Exception = Erfolg", genau das
Muster, das dieses Modul schon mehrfach eingeholt hat (siehe 1.9.43.0/1.9.44.0,
Remove-DbaDatabase bei Invoke-sqmRestoreDatabase).
Fix: nach der Grant-/Revoke-Anweisung wird IS_SRVROLEMEMBER() explizit nachgeprueft; weicht das
Ergebnis vom erwarteten Zustand ab (inkl. NULL), wirft die Funktion jetzt einen expliziten
Fehler mit der vermuteten Ursache, statt faelschlich Erfolg zu meldem. Regressionsgetestet gegen
DEV01 (normaler Grant+Revoke-Rundlauf funktioniert weiterhin unveraendert), NULL-Verhalten von
IS_SRVROLEMEMBER() fuer einen nicht aufloesbaren Login separat verifiziert.
Nachtrag zu 1.9.53.0/1.9.54.0: auf SQL01 manuell vorab deaktiviert getestet ("New
Login_Enforce Passwort Policy" aus, unser Code also gar nicht im Spiel) - der Grant scheiterte
trotzdem identisch am selben Trigger-Rollback. Damit war ausgeschlossen, dass es an unserer
DisablePolicy-Logik liegt: entweder eine ZWEITE, aktive Policy mit demselben Auswertungsmodus,
oder der Trigger-Mechanismus selbst - in beiden Faellen hilft das gezielte Deaktivieren einer
einzelnen benannten Policy nicht.
Gegen DEV01 mit einem synthetischen Test-Trigger (FOR ALTER_LOGIN, CREATE_LOGIN, ROLLBACK) nachgestellt und verifiziert, dass ein Policy-unabhaengiger Trigger genau den gemeldeten Fehler reproduziert ("The transaction ended in the trigger. The batch has been aborted." bzw. "Die Transaktion endete mit dem Trigger.").
Fix: Invoke-sqmTempSysadminAction deaktiviert jetzt zusaetzlich (Default -DisableServerTriggers
$true) ALLE aktuell aktivierten serverweiten DDL-Trigger fuer die Dauer der Aktion und stellt in
einem finally-Block GENAU den vorherigen Zustand wieder her (kein pauschales "alle wieder an") -
unabhaengig davon, welche/wie viele Policies aktiv sind oder ob der Trigger CREATE LOGIN,
ALTER SERVER ROLE oder etwas anderes abfaengt. Die bisherige DisablePolicy-Logik bleibt als
zweite, unabhaengige Absicherung bestehen. Ein fehlgeschlagenes Wiederaktivieren eines Triggers
wird zusaetzlich als Eventlog-Fehler eskaliert (Governance-relevant).
dbcreator-Rolle zusaetzlich zu Windows-Logins/sysadminGrant-sqmTemporarySysadmin/Invoke-sqmTempSysadminAction unterstuetzten bisher ausschliesslich
Windows-/AD-Logins und ausschliesslich die sysadmin-Rolle. Neu:
-Role ('sysadmin' Default, oder 'dbcreator') auf beiden Funktionen - die
ALTER SERVER ROLE-Anweisung ist jetzt parametrisiert statt fest auf [sysadmin] verdrahtet.-CreateLoginIfMissing) bleibt auf
Windows-/AD-Logins beschraenkt (CREATE LOGIN ... FROM WINDOWS ergibt fuer SQL-Logins keinen
Sinn ohne Kennwortvorgabe), mit klarer Fehlermeldung statt stillschweigendem Fehlschlag.sqmTempDbcreator_... statt nur sqmTempSysadmin_...),
damit z.B. ein temporaeres sysadmin und ein temporaeres dbcreator fuer denselben Login
nebeneinander bestehen koennen.Verifiziert gegen DEV01: -Role dbcreator fuer einen bestehenden SQL-Login (sqmTestLogin) im
vollen Grant+Revoke-Rundlauf (inkl. Trigger-Deaktivierung/-Wiederherstellung), sowie
Grant-sqmTemporarySysadmin akzeptiert einen bloßen SQL-Loginnamen jetzt ohne den frueheren
Windows-Format-Fehler.
Install.ps1 scheiterte unter PowerShell 5.1 beim Lesen des dbatools-Versions-CapsNachtrag zu 1.9.55.0: Select-Object -ExpandProperty MaximumVersion auf einem
RequiredModules-Eintrag (eine Hashtable) warf unter echtem powershell.exe 5.1 "Die
MaximumVersion-Eigenschaft kann nicht gefunden werden." - -ExpandProperty findet Hashtable-Keys
in Windows PowerShell 5.1 nicht (Adapter-Unterschied), waehrend derselbe Code unter PowerShell 7
klaglos durchlief. Genau die Art Bug, die beim Entwickeln/Testen unter pwsh unsichtbar bleibt -
der Import-Test des pre-push-Hooks laeuft zwar unter echtem PS 5.1, fuehrt Install.ps1 selbst
aber nicht aus, daher ist das durchgerutscht.
Fix: direkter Punkt-Zugriff (.MaximumVersion) auf das per Select-Object -First 1 gefilterte
Objekt statt -ExpandProperty - funktioniert in beiden PowerShell-Versionen. Gesamte
dbatools-Versionspruefung end-to-end unter echtem powershell.exe 5.1 (5.1.26100.8875)
nachgestellt und verifiziert (Cap-Ermittlung, installierte Version, PSGallery-Vergleich).
DbatoolsSharePath-Konfigurationsschluessel fuer die dbatools-FreigabeNachtrag zu 1.9.56.0: der Pfad zur Kunden-Freigabe mit der dbatools + dbatools.library-Baseline
wurde bisher implizit aus -Source hergeleitet (Geschwisterordner Modules neben Tools) -
funktioniert nur, wenn diese Ordnerkonvention exakt so vorliegt, und laesst sich nicht explizit
setzen/uebersteuern.
Neu: Set-sqmConfig -DbatoolsSharePath '<Pfad>' (neutraler Default $null, Kunden-Default
<Freigabe>\SQLSources\Modules - analog zum bereits vorhandenen
SsrsInstallerPath). Install.ps1 Schritt 5b liest diesen Key jetzt vorrangig (direkt aus
config.json, da das Modul an dieser Stelle noch nicht importiert ist - dbatools muss zuerst da
sein) und faellt nur auf die bisherige Herleitung zurueck, wenn der Key noch nicht gesetzt ist.
Nach erfolgreichem Modulimport wird ein neu ermittelter Pfad automatisch in die Konfiguration
zurueckgeschrieben (Schritt 6a) - kuenftige Installationen und andere Modulfunktionen finden ihn
dann direkt ueber Get-sqmConfig, ohne ihn erneut herzuleiten.
Verifiziert: Default leer ausserhalb der Kundenumgebung, Set-sqmConfig persistiert korrekt nach
config.json, Wert bleibt nach einem frischen Modul-Reimport erhalten. Direktes
config.json-Parsing (Schluessel fehlt / ist null / ist gesetzt) fuer alle drei Faelle geprueft.
Nachtrag zu 1.9.55.0: der dortige PSGallery-basierte Versionscheck ist fuer beim Kunden gehostete
Instanzen (wie der betroffene Produktivserver) der falsche Weg - PSGallery ist auf einer abgeschotteten
Produktivinstanz nicht erreichbar. Unter <SQLSources>\Modules (Geschwisterordner von Tools,
wo sqmSQLTool selbst per UNC-Installation herkommt) liegt dort bereits ein vom Kunden gepflegtes,
fertiges Ordnerpaar dbatools + dbatools.library (das binaere Begleitmodul von dbatools 2.x).
Fix: Schritt 5b erkennt eine Kunden-Installation jetzt VOR dem PSGallery-Zweig (gleiche Erkennung
wie die bestehende Kunden-Konfiguration in Schritt 7 - die Erkennung wird jetzt einmalig
berechnet und in beiden Schritten wiederverwendet statt dupliziert). Ist der abgeleitete
Modules-Pfad relativ zu -Source vorhanden und enthaelt beide Ordner, werden dbatools und
dbatools.library per robocopy /MIR in den Ziel-Scope gespiegelt - PSGallery bleibt der Weg
fuer alle anderen (Nicht-Kunden-)Umgebungen. Pfad-Ableitung gegen die reale
InstallSourcePath-Konfiguration des betroffenen Servers verifiziert
(\\<Freigabe>\SQLSources\Tools\sqmSQLTool-main ->
...\SQLSources\Modules, exakter Treffer).
Grant-sqmTemporarySysadmin fragt standardmaessig nicht mehr interaktiv nachConfirmImpact = 'High' liess PowerShell bei jedem Aufruf automatisch die "Moechten Sie diesen
Vorgang ausfuehren?"-Rueckfrage einblenden (Default $ConfirmPreference = 'High') - Reibung bei
ticketbasierten oder skriptgesteuerten Aufrufen (z.B. aus einer Automatisierung ohne interaktive
Session, wo die Abfrage schlicht haengen bleiben wuerde). -TicketNumber ist bereits der Nachweis
einer bewussten Entscheidung.
Fix: $ConfirmPreference = 'None' intern gesetzt - ConfirmImpact = 'High' bleibt als Metadatum
bestehen (weiterhin zurecht als hochriskante Aktion dokumentiert), aber es wird nicht mehr
automatisch nachgefragt. -WhatIf funktioniert unveraendert; wer die Rueckfrage bewusst will,
kann -Confirm explizit anhaengen. Verifiziert gegen DEV01: -WhatIf zeigt weiterhin die Absicht
ohne Aenderung, ein echter Aufruf ohne -Confirm laeuft ohne Prompt/Haengenbleiben durch.
Install.ps1 erkennt und aktualisiert veraltete dbatools-InstallationenAuf SQL01 stand seit 2022 unbemerkt dbatools 1.1.95 - mehrere Major-Versionen veraltet,
sehr wahrscheinlich Ursache fuer eine abweichende Get-DbaPbmPolicy-Rueckgabeform, die aeltere
Codepfade in Set-sqmSqlPolicyState nicht abdeckten. Die bisherige Pruefung war rein binaer
("Ordner vorhanden -> ok") und haette das nie aufgedeckt, unabhaengig davon wie alt die Version war.
Fix: Schritt 5b prueft jetzt zusaetzlich die installierte Version gegen die auf PSGallery neueste
verfuegbare (innerhalb des bestehenden MaximumVersion-Caps aus RequiredModules, liest also den
Cap direkt aus sqmSQLTool.psd1 statt ihn zu duplizieren) und aktualisiert bei Rueckstand
automatisch. Ist PSGallery nicht erreichbar (typisch fuer eine abgeschottete Produktivinstanz),
wird die erkannte installierte Version als Warnung ausgegeben statt stillschweigend als "ok"
durchzugehen. Verifiziert (Versionserkennung + PSGallery-Vergleich) gegen die hier installierte
dbatools 2.8.2 - erkannte korrekt 2.8.4 als neuere verfuegbare Version.
Invoke-sqmTempSysadminAction - Fehlschlag der Policy-Deaktivierung war unsichtbarNachtrag zu 1.9.53.0: auf SQL01 (DefaultPolicy korrekt auf 'New Login_Enforce
Passwort Policy' gesetzt, passend zum dort per sp_syspolicy_add_policy angelegten Server/Login-
Facet und dem serverweiten syspolicy_server_trigger FOR ALTER_LOGIN, CREATE_LOGIN - ALTER SERVER
ROLE ADD MEMBER loest intern ebenfalls ein ALTER_LOGIN-Ereignis aus, der Trigger sollte also
weiterhin feuern) schlug der sysadmin-Grant trotz 1.9.53.0 weiterhin fehl.
Set-sqmSqlPolicyState -State Disable wurde bisher mit -ContinueOnError aufgerufen: schlaegt das
Deaktivieren selbst fehl (z.B. Berechtigung, dbatools/SMO-Eigenheit), wurde das bisher komplett
stillschweigend uebersprungen - kein Log-Eintrag, keine Warnung. Der anschliessende sysadmin-Grant
lief dann gegen die weiterhin aktive Policy und scheiterte am selben Trigger-Rollback wie ganz ohne
Deaktivierungsversuch, ohne dass aus dem Log je erkennbar war, WARUM die Deaktivierung nicht griff.
Fix: der Rueckgabewert von Set-sqmSqlPolicyState wird jetzt in jedem Fall ausgewertet - bei
Status ungleich 'Success' wird eine WARNUNG mit der eigentlichen Fehlermeldung geloggt, bevor der
Grant/Revoke-Versuch trotzdem weiterlaeuft. Regressionstest (Grant + Revoke Rundlauf) erneut gegen
DEV01 verifiziert.
Invoke-sqmTempSysadminAction scheiterte auf Instanzen mit "On Change: Prevent"-PBM-Policy an ALTER SERVER ROLEAuf einem Kunden-Produktivserver schlug Grant-sqmTemporarySysadmin fehl mit "The transaction
ended in the trigger. The batch has been aborted." - Ursache: der eingebaute
syspolicy_server_trigger (Policy-Based Management) fing die ALTER SERVER ROLE [sysadmin] ADD
MEMBER-Anweisung ab und rollte sie zurueck, weil dort eine Policy mit Auswertungsmodus
"On Change: Prevent" aktiv ist. Kein Kennwortrichtlinien-Thema (der betroffene Login ist ohnehin
ein Windows-/AD-Login, fuer den SQL Server gar keine Kennwortrichtlinie fuehrt) - dieser
serverweite Trigger kann JEDE DDL_SERVER_LEVEL_EVENTS-Anweisung abfangen.
Der Bug: die vorhandene Policy-Deaktivierung (DefaultPolicy via Set-sqmSqlPolicyState) war nur
um das optionale CREATE LOGIN gelegt, nicht um ALTER SERVER ROLE ADD/DROP MEMBER oder
DROP LOGIN - also nicht um die eigentliche sysadmin-Vergabe/-Entziehung selbst. Schwerwiegender
als der Grant-Fehlschlag: der automatische Revoke-Job Tage spaeter waere auf einer solchen Instanz
genauso gescheitert und haette sich (Job wird bei Fehler bewusst NICHT geloescht) nicht selbst
entfernt - der Login waere unbefristet sysadmin geblieben.
Fix: die Policy-Deaktivierung umschliesst jetzt die GESAMTE Grant-/Revoke-Aktion (Login-Anlage +
Rollenaenderung + optionales Login-Entfernen), garantiert wieder aktiviert per finally auch bei
Fehlern. Regressionstest (Grant + Revoke Rundlauf) gegen DEV01 verifiziert - DEV01 hat selbst keine
PBM-Policy konfiguriert, das eigentliche Trigger-Szenario liess sich also nur indirekt bestaetigen
(Root Cause + Fix-Scope, nicht der volle Fehlerfall selbst).
Get-sqmLoginSettings mit sysadmin-Mitgliedschaft und Sicherheitsampel (RiskLevel/RiskIcon)Bisher zeigte der Report nur die rohen Kennwort-Einstellungen, ohne einzuordnen, wie kritisch eine Abweichung tatsaechlich ist - ein deaktivierter Kennwortablauf bei einem sysadmin-Login ist ein ganz anderes Risiko als bei einem eingeschraenkten Konto.
Neu: IsSysAdmin (Mitgliedschaft in der festen Serverrolle sysadmin, via sys.server_role_members
+ sys.server_principals - gilt fuer SQL- UND Windows-Logins/-Gruppen gleichermassen) sowie ein
daraus abgeleitetes RiskLevel ('Critical' / 'Warning' / 'OK' / 'N/A') mit kompaktem RiskIcon
(🔴/🟡/🟢/⚪) fuer eine schnelle visuelle Einordnung, wenn Text in der Tabelle/dem Report zu lang
wird: Kennwort-Richtlinie oder -Ablauf deaktiviert bei einem sysadmin-Login ist Critical, beim
Nicht-sysadmin nur Warning, ohne Abweichung OK. Gilt nur fuer SQL-Logins - bei Windows-Logins/
-Gruppen greift die Kennwortrichtlinie von AD statt SQL Server, dort N/A statt eines falschen
Befundes (unabhaengig von deren sysadmin-Mitgliedschaft, die durchaus haeufig ist, z.B. die
SQL-Dienstkonten selbst).
Gegen DEV01 verifiziert: dev/sa (sysadmin, Policy+Expiration aus) → Critical, sqmTestLogin
(kein sysadmin, gleiche Einstellungen) → Warning, alle Windows-Logins/-Gruppen (inkl. sysadmin-
Mitgliedern wie den SQL-Dienstkonten) → N/A statt Fehlklassifikation.
Start-sqmToolGui.cmd - fragilen zweiten Elevation-Hop entferntNachtrag zu 1.9.50.0: auf DEV03 direkt aus einer bereits elevated Shell gestartet lief
Start-sqmToolGui.ps1 (powershell -File ...) einwandfrei durch - der UAC-Doppelklick-Weg
brach aber weiterhin kommentarlos ab. Eingrenzung damit auf die Elevation-Kette selbst: der
bisherige Weg elevierte cmd.exe (Start-Process cmd.exe -ArgumentList '/c "..."' -Verb RunAs),
das dann Start-sqmToolGui.cmd von vorn komplett neu durchlief (zweiter net session-Check,
dann erst der eigentliche powershell -File-Aufruf) - irgendwo in diesem zweiten Hop brach es ab,
ohne dass unsere eigene Fehlerbehandlung in Start-sqmToolGui.ps1 je erreicht wurde (die griff ja
erst NACH diesem Hop).
Fix: powershell.exe wird jetzt direkt elevated (Start-Process powershell.exe -ArgumentList
'... -File "..."' -Verb RunAs), nicht mehr ueber ein sich selbst neu aufrufendes cmd.exe. Der
elevated Pfad ist damit strukturell identisch zu dem bereits erfolgreich getesteten manuellen
Aufruf aus einer elevated Shell - nur die Elevation selbst kommt jetzt per UAC statt manuell.
Start-sqmToolGui.cmd konnte auf DEV03 nach dem UAC-"Zulassen" kommentarlos beendenFunktionierte in der Produktionsumgebung, auf DEV03 dagegen: UAC-Abfrage erscheint, kurzes
Fensteraufblitzen, dann nichts mehr - ohne Fehlermeldung. Ursache liess sich von hier aus nicht
live nachstellen (kein interaktiver Desktop-Zugriff, eigene Tool-Sessions laufen nicht elevated),
aber der bisherige Code hatte eine bekannte Luecke: das try { Import-Module ...; Show-sqmToolGui }
catch {...} in der alten -Command-Zeile faengt zuverlaessig nur SYNCHRONE Fehler ab (Modul fehlt,
Fehler waehrend des GUI-Aufbaus vor ShowDialog()). Eine Ausnahme, die dagegen WAEHREND der
ShowDialog()-Message-Loop auftritt (z.B. in einem Event-Handler beim Befuellen der Parameterfelder),
erreicht dieses catch nicht zuverlaessig - unter powershell.exe als WinForms-Host greift dafuer
.NETs eigene Unhandled-Exception-Policy fuer den UI-Thread, die den Prozess je nach Konfiguration
kommentarlos beenden kann, ohne dass die Ausnahme je den aufrufenden try/catch erreicht. Das erklaert
genau das beobachtete Bild: kurzes Aufblitzen, dann Ende, keine Fehlermeldung.
Fix: Die Startlogik ist jetzt in einer eigenen Start-sqmToolGui.ps1 (liegt neben dem .cmd,
wird von Install.ps1 mit ausgeliefert) statt als gequoteter -Command-Einzeiler. Sie setzt
[Application]::SetUnhandledExceptionMode('CatchException') + einen ThreadException-Handler,
damit auch asynchrone GUI-Ausnahmen sicher abgefangen werden - jeder Fehler (synchron oder aus der
Message-Loop) wird jetzt nach %ProgramData%\sqmSQLTool\gui-launch.log geschrieben UND als
MessageBox angezeigt, statt den Prozess wortlos zu beenden. -WindowStyle Minimized vorerst auf
Normal zurueckgesetzt (ein sichtbares Fenster ist beim Verifizieren dieses Fixes wichtiger als das
AV/EDR-Argument fuer ein verstecktes Fenster) - kann zurueckgestellt werden, sobald bestaetigt ist,
dass das Problem behoben ist.
Show-sqmToolGui vergroessert - mehr Platz fuer die Hilfe-AnzeigeDer Help-Button schreibt Get-Help -Full in dasselbe "Output"-Panel wie die
Kommando-Ausgabe (unterste Zeile des rechten TableLayoutPanel) - bei der bisherigen
Fenstergroesse (1150x720) und dem 55/45-Split zugunsten von "Parameters" blieb dafuer
wenig Raum, obwohl Bildschirmaufloesungen heute deutlich mehr hergeben.
Standardgroesse von 1150x720 auf 1400x900 erhoeht (Minimum 900x560 -> 1000x650), und der
Hoehen-Split der unteren beiden Prozent-Zeilen von 55% Parameters / 45% Output auf 40%/60%
zugunsten von Output gedreht - die Parameter-Liste bleibt ohnehin scrollbar (paramPanel.AutoScroll)
und braucht selten mehr als eine Handvoll Zeilen, waehrend Get-Help -Full-Text schnell laenger wird.
Assets\sqmSQLTool.ico (neu, 16/32/48/256px, PNG-in-ICO) - ein einfaches, im Marken-Farbschema
von powershelldba.de gehaltenes Datenbank-Zylinder-Symbol (dunkles Navy-Badge, Akzentblau
#2e86c1/#5dade2) statt des Standard-.cmd-Icons. Install.ps1 kopiert die Datei bei einer
AllUsers-Installation zusammen mit dem Launcher nach C:\Program Files\sqmSQLTool und setzt
sie als IconLocation der Startmenue-Verknuepfung. Assets\ ist vom robocopy-Modul-Payload
(Schritt 4) ausgenommen - die Datei wird gezielt fuer den Launcher kopiert, nicht als Modulinhalt.
Kein automatisches Anheften an Start: der fruehere Trick (FolderItem.InvokeVerb('pintostartscreen')
ueber Shell.Application) wurde von Microsoft fuer Desktop-Verknuepfungen seit Windows 10 2004 /
Windows 11 entfernt und liefe hier nur als stiller No-Op, ohne unterstuetzte Ersatz-API. Install.ps1
gibt daher nur einen Hinweis auf den einmaligen manuellen Schritt aus (Rechtsklick > "An Start
anheften"), statt eine Automatisierung vorzutaeuschen, die auf dieser OS-Version nichts bewirkt.
Get-sqmLoginSettings zeigt jetzt Kennwort-Richtlinie, Kennwortablauf und "Kennwort muss geaendert werden"Bisher fehlten in der Ausgabe die Kennwort-bezogenen Einstellungen eines SQL-Logins
komplett - man musste dafuer separat sys.sql_logins/LOGINPROPERTY() abfragen.
Neu (per LEFT JOIN sys.sql_logins + LOGINPROPERTY(), zusaetzlich zu den bisherigen
Spalten): PasswordPolicyEnforced (CHECK_POLICY), PasswordExpirationEnforced
(CHECK_EXPIRATION), PasswordExpired (Kennwort aktuell abgelaufen), MustChangePassword
("Benutzer muss Kennwort bei naechster Anmeldung aendern") und DaysUntilExpiration.
Bei Windows-Logins/-Gruppen sind diese Felder $null (kein sys.sql_logins-Eintrag,
Kennwortrichtlinie liegt dort bei AD) - Invoke-DbaQuery liefert das ohne -As PSObject
als [DBNull], nicht als PowerShell-$null; ein direktes [bool]-Cast waere daher auf
einen [DBNull]-Wert gelaufen und mit einer Ausnahme gescheitert. Gegen DEV01 verifiziert:
SQL-Logins liefern die echten Werte, Windows-Logins bleiben leer statt zu fehlern.
Show-sqmToolGui (elevated) + AllUsers-Startmenue-EintragBisher musste die GUI manuell in einer PowerShell-Session gestartet werden
(Import-Module sqmSQLTool; Show-sqmToolGui). Viele der darueber aufrufbaren Funktionen
(Invoke-sqmNtfsSetup, Eventlog-Quellen, SQL-Agent-Jobs) aendern lokale Systemzustaende
und brauchen dafuer Adminrechte - ein einfacher Doppelklick ohne Elevation waere nur
eingeschraenkt nutzbar gewesen.
Neu: Start-sqmToolGui.cmd (Repo-Root, analog zu Install.cmd) prueft net session und
relauncht sich bei Bedarf per UAC selbst elevated, startet dann Show-sqmToolGui in einem
minimierten PowerShell-Fenster (bewusst -WindowStyle Minimized statt Hidden - ein
komplett verstecktes PowerShell-Fenster ist ein klassisches AV/EDR-Alarmsignal auf
ueberwachten SQL-Servern). Schlaegt der Modul-Import fehl, zeigt eine MessageBox den
Fehler statt wortlos im minimierten Fenster zu verschwinden.
Install.ps1 kopiert den Launcher bei einer AllUsers-Installation zusaetzlich nach
C:\Program Files\sqmSQLTool\Start-sqmToolGui.cmd (bewusst ausserhalb des per
robocopy /PURGE verwalteten Modul-Zielordners) und legt eine AllUsers-Startmenue-
Verknuepfung sqmSQLTool GUI.lnk an - nicht fatal, falls das fehlschlaegt (die GUI bleibt
weiterhin manuell startbar).
Invoke-sqmUserDatabaseBackup legte ein neues Backupverzeichnis an, konnte aber danach nicht hineinschreibenZeigte die Server-Eigenschaft BackupDirectory auf einen noch nicht existierenden Pfad (z.B. nach
einer Umstellung waehrend eines Instanz-Setups, ohne dass fuer den neuen Pfad je Invoke-sqmNtfsSetup
gelaufen ist), legte New-Item das Verzeichnis zwar erfolgreich unter der aufrufenden Identitaet an
- ein neu angelegter Ordner erbt NTFS-Rechte aber nur, wenn ein Vorfahre bereits eine vererbbare ACE
fuer das SQL-Dienstkonto hat. BACKUP DATABASE laeuft dagegen als SQL-Dienstkonto und scheiterte
danach mit "Cannot open backup device ... Operating system error 5(Access is denied.)", obwohl das
Verzeichnis sichtbar vorhanden war.
Fix: Nach dem Anlegen eines neuen Backupverzeichnisses vergibt die Funktion jetzt automatisch
Modify-Rechte fuer das/die SQL-Dienstkonto(s) auf diesem Verzeichnis (ueber Invoke-sqmNtfsSetup
-Directory $BackupPath -Permission Modify -SkipBackup), bevor der eigentliche Backup-Lauf startet.
Schlaegt das Setzen der Rechte selbst fehl (z.B. Dienstkonto nicht ermittelbar), wird das nur als
Warnung geloggt - das Backup wird trotzdem versucht und liefert im Fehlerfall die eigentliche
SQL-Fehlermeldung.
Show-sqmToolGui forderte bei Funktionen mit mehreren Parameter-Sets alle sich gegenseitig ausschliessenden Pflichtparameter gleichzeitigBei Invoke-sqmRestoreDatabase z.B. ist -BackupFile nur im Parameter-Set SingleFile und
-BackupFiles nur im Set Sequence Mandatory - beides sind Alternativen, nicht zwei zusaetzliche
Pflichtfelder. Die bisherige Pruefung vor Run markierte einen Parameter aber bereits als Pflichtfeld,
sobald IRGENDEINES seiner ParameterAttribute-Objekte Mandatory = $true gesetzt hatte - unabhaengig
vom Parameter-Set. Dadurch verlangte der Dialog -BackupFile UND -BackupFiles gleichzeitig ausgefuellt,
obwohl Invoke-sqmRestoreDatabase bei beiden gesetzt sofort mit "Parameter set cannot be resolved"
fehlgeschlagen waere - der Restore liess sich ueber die GUI faktisch nie starten.
Fix: Die Validierung nutzt jetzt Command.ParameterSets und laesst Run zu, sobald FUER MINDESTENS
EIN Parameter-Set alle seine Pflichtparameter ausgefuellt sind (genau wie PowerShell selbst beim
Binden entscheidet), statt die Vereinigung aller Pflichtparameter ueber alle Sets hinweg zu verlangen.
Die Hinweis-Meldung bei fehlenden Angaben zeigt bei mehreren Parameter-Sets jede noch unvollstaendige
Kombination einzeln an, damit klar ist, dass EINE davon ausreicht.
Invoke-sqmRestoreDatabase meldete einen fehlgeschlagenen Secondary-Drop faelschlich als ErfolgNachtrag zu 1.9.43.0: In einem realen AG-Restore (SQL01/SQL02, Datenbank
AlwaysOnTest) lief der Secondary-Drop zwar an, Remove-DbaDatabase scheiterte dabei aber mit
"Cannot drop the database 'AlwaysOnTest', because it does not exist or you do not have
permission." - ohne eine Exception zu werfen, auch nicht mit -EnableException. Remove-DbaDatabase
faengt einen fehlgeschlagenen Drop pro Datenbank intern ab und packt den rohen SQL-Fehlertext
lediglich in die Status-Eigenschaft des Rueckgabeobjekts. Der bisherige Code werteten diesen
Rueckgabewert nicht aus und meldete "RemoveFromSecondary: Success", obwohl der Drop nachweislich
fehlgeschlagen war.
Zusaetzlich verwendete die vorausgehende Existenzpruefung eine von dbatools ueber die
PowerShell-Session hinweg gecachte SMO-Verbindung (Connect-DbaInstance ohne
-NonPooledConnection): deren .Databases-Collection kann den Stand von vor dem eigentlichen Drop
zeigen, selbst wenn sich die Datenbank auf dem Server laengst geaendert hat.
Fix: Connect-DbaInstance fuer die Existenzpruefung nutzt jetzt -NonPooledConnection fuer eine
frische Sicht. Der Rueckgabewert von Remove-DbaDatabase wird ausgewertet: Status -eq 'Dropped'
gilt als Erfolg, Status -match 'does not exist' gilt als bereits erreichtes Ziel (Datenbank ist
weg, kein Fehler), jeder andere Status (z.B. ein echtes Berechtigungsproblem) wird als Failed
gemeldet statt verschluckt zu werden.
Invoke-sqmRestoreDatabase liess bei AG-Restore stale Datenbank auf Secondary zurueckDer Secondary-Cleanup-Schritt (nach Remove-DbaAgDatabase auf dem Primary) pruefte vor dem
Remove-DbaDatabase-Aufruf zusaetzlich .IsAccessible der Datenbank auf dem sekundaeren Knoten.
Nach dem Entfernen aus der AG liegt die dort verbleibende Kopie aber typischerweise im Status
RESTORING - IsAccessible ist dann $false, obwohl die Datenbank sehr wohl existiert. Der Code
interpretierte das faelschlich als "nicht vorhanden" und uebersprang den Drop komplett (inklusive
irrefuehrender VERBOSE-Meldung). Beim anschliessenden Wiedereinfuegen in die AG
(Add-DbaAgDatabase -SeedingMode Automatic) schlug das Seeding dadurch mit
"Database With Name Already Exists" fehl - die Secondaries blieben ohne die neu restaurierte
Datenbank zurueck, obwohl RejoinAG scheinbar der einzige fehlgeschlagene Schritt war.
Fix: Existenzpruefung auf dem Secondary vor dem Drop prueft nur noch, ob die Datenbank ueberhaupt
vorhanden ist ($secondaryServer.Databases[$finalDbName]), nicht mehr zusaetzlich IsAccessible.
Invoke-sqmMonitoringKey schrieb/las den falschen Registry-PfadDie Quelldatei war auf HKLM:\<RegistryBase>\dtcSoftware\sqmSQLTool zurueckgefallen, obwohl die
reale System-Center-Konvention beim Kunden HKLM:\SYSTEM\<Kunde>\Systemcenter lautet, mit den Werten
SQL (0/1/2 - None/Standard/Full, kundenabhaengig und manuell zu setzen) und
SQLFreeSpaceVersion (Standard/Cluster, per -AutoDetectSQLFreeSpaceVersion ueber die
AG-Zugehoerigkeit automatisch erkennbar) direkt unter diesem Schluessel. Die gebaute/verteilte
Kopie in bin/Public hatte weiterhin den richtigen Pfad - nur die Quelle war betroffen.
Fix: $regSubKey wieder auf $RegistryBase\<Kunde>\SystemCenter gesetzt. Neuer Pester-Test
(Invoke-sqmMonitoringKey.Tests.ps1, komplett gemockt) haelt den Pfad fest und schlaegt
nachweislich fehl, sobald der alte dtcSoftware\sqmSQLTool-Pfad zurueckkommt.
Repair-sqmServerName (Write-Error unter $ErrorActionPreference='Stop')Der GitHub-Actions-Job "Import & Pester (PowerShell 7)" schlug bei Repair-sqmServerName.Tests.ps1
fehl: pwsh-Steps in GitHub Actions laufen mit $ErrorActionPreference = 'Stop'. Die Catch-Bloecke
riefen Write-Error ohne eigenes -ErrorAction auf - unter geerbtem Stop wurde daraus ein
TERMINIERENDER Fehler statt einer reinen Meldung. Im inneren Catch (sp_addserver-Fehlschlag) brach
das die Ausfuehrung ab, bevor return $result erreicht wurde; der Fehler wanderte in den aeusseren
Catch, dessen eigener Write-Error-Aufruf dann komplett unbehandelt aus der Funktion entkam - der
Test scheiterte nicht an der Assertion, sondern weil Repair-sqmServerName selbst eine Exception
warf, statt wie dokumentiert ein Ergebnisobjekt mit Status = 'Error' zurueckzugeben. Lokal (ohne
Stop-Preference) blieb das unbemerkt, weil Write-Error dort nur eine nicht-terminierende Meldung
schreibt.
Fix: Alle betroffenen Write-Error-Aufrufe (Repair-sqmServerName, sowie Get-sqmSsasDeploymentMode
und Set-sqmSsasDeploymentMode mit demselben Muster) bekommen jetzt explizit -ErrorAction Continue,
damit sie unabhaengig von der geerbten Preference des Aufrufers immer nur melden statt abzubrechen -
mit $ErrorActionPreference = 'Stop' reproduziert und verifiziert, komplette Testsuite (202 Tests)
weiterhin gruen.
Invoke-sqmSplunkConfiguration meldete "neu gestartet", obwohl der Neustart fehlgeschlagen warPraxisfall auf HLB1W01AWSA0087: Restart-Service -Name SplunkForwarder -Force warf "Cannot stop
SplunkForwarder service", der Dienst blieb danach im Status Stopped. Im Log stand trotzdem
"Dienst 'SplunkForwarder' neu gestartet." und das Endergebnis meldete Status = Success - weil
Restart-Service ohne -ErrorAction Stop aufgerufen wurde und _sqmSplunk_LocalCore mit
$ErrorActionPreference = 'Continue' laeuft: der nicht-terminierende Fehler landete nie im
catch-Block, die Ausfuehrung lief einfach zur naechsten Zeile durch.
Fix: Restart-Service bekommt jetzt -ErrorAction Stop, damit ein fehlgeschlagener Stop-Schritt
tatsaechlich im catch ankommt und korrekt als Fehler geloggt wird. Zusaetzlich versucht die
Funktion in diesem Fall, den Dienst per Start-Service nachzustarten - Restart-Service kann ihn
bereits gestoppt haben, bevor der Fehler auftrat, und ohne Nachstart-Versuch waere er stehen
geblieben, obwohl er vorher lief.
Invoke-sqmSplunkConfiguration liess veraltete Pfade nach SQL-Versionswechsel im Environment stehenDie Funktion hat gesetzte MSSQLn_Log-Variablen grundsaetzlich nie ueberschrieben. Wurde eine
Instanz auf eine neue SQL-Version aktualisiert (z.B. neues ErrorLog-Verzeichnis wie
MSSQL16.MSSQLSERVER statt MSSQL15.MSSQLSERVER) oder komplett neu installiert, blieb der alte,
nicht mehr existierende Pfad in der Umgebungsvariable stehen - Splunk ueberwachte damit dauerhaft
einen toten Pfad und meldete Fehlalarme.
Fix: Vor dem "nicht ueberschreiben" wird jetzt geprueft, ob der gespeicherte Pfad noch existiert
(Test-Path). Existiert er nicht mehr, gilt die Variable als veraltet und wird mit dem aktuell
ermittelten ErrorLog-Pfad der Instanz aktualisiert - eine noch gueltige, unveraenderte Variable
wird weiterhin nicht angefasst. Zusaetzlich werden MSSQLn_Log-Variablen entfernt, deren Ordinalzahl
groesser ist als die Anzahl aktuell installierter Instanzen (verwaist, z.B. nach Deinstallation
statt In-Place-Upgrade einer Instanz). Die Instanznamen werden vor der Nummerierung sortiert, damit
die Ordinalzahl bei unveraendertem Instanzbestand stabil bleibt. -Mode Test meldet veraltete und
verwaiste Variablen jetzt ebenfalls, ohne etwas zu aendern.
Repair-sqmServerNameNach einer Windows-Hostname-/FQDN-Aenderung (Umbenennung, VM-Klon, Rechenzentrums-Umzug) meldet
Windows sofort den neuen Namen, waehrend SQL Server den ALTEN Namen weiter als "lokalen Server"
in sys.servers fuehrt (@@SERVERNAME / SERVERPROPERTY('ServerName')). Der Drift faellt oft
erst auf, wenn Replikation, Linked-Server-Loopbacks, SSRS oder Zertifikate mit dem falschen Namen
fehlschlagen - bislang gab es dafuer keine sqmSQLTool-Funktion.
Die neue Funktion vergleicht den registrierten Namen mit dem aus MachineName/InstanceName
berechneten Soll-Namen und korrigiert ihn per sp_dropserver/sp_addserver. Vor der Aenderung
prueft sie Failover-Cluster-Mitgliedschaft (IsClustered), AlwaysOn-AG-Mitgliedschaft
(sys.availability_replicas) und Replikationsrollen (sys.databases: Distributor/Publisher/
Subscriber) - in all diesen Faellen haengen andere Objekte am alten Namen, deshalb wird ohne
-Force abgebrochen (Status Blocked). Unterstuetzt -WhatIf; weist nach erfolgreicher
Aenderung darauf hin, dass ein Dienst-Neustart noetig ist, bis @@SERVERNAME den neuen Wert
liefert (kein automatischer Neustart).
Set-sqmSsasDeploymentMode schlug mit "Invalid query" fehl bei -InstanceName im SSMS-FormatWurde die Instanz wie in SSMS ueblich als "Server\Instanz" angegeben (z. B. HLESDSQL001\APSM,
per Copy-Paste aus der Verbindungsleiste), baute die Funktion daraus den Dienstnamen
MSOLAP$HLESDSQL001\APSM und damit die WQL-Abfrage SELECT * FROM Win32_Service WHERE
Name='MSOLAP$HLESDSQL001\APSM'. Der Backslash ist in WQL-String-Literalen ein Escape-Zeichen -
\A ist keine gueltige Escape-Sequenz, Get-CimInstance bricht deshalb mit "Invalid query" ab.
Fix: Ein Server-Praefix vor dem letzten Backslash wird jetzt automatisch entfernt, es zaehlt nur der Instanzname dahinter (Windows-Dienstnamen enthalten ohnehin nie den Servernamen).
Get-sqmSsasDeploymentModeRein lesendes Gegenstueck zu Set-sqmSsasDeploymentMode - liefert den aktuellen DeploymentMode
(Multidimensional/Tabular/SharePoint) einer SSAS-Instanz aus msmdsrv.ini, ohne etwas zu aendern.
Akzeptiert ebenfalls "Server\Instanz" als -InstanceName.
Invoke-sqmRestoreTest fragte immer nach -DatabaseName, auch wenn -BackupFile schon angegeben war-DatabaseName war unbedingt Mandatory = $true - PowerShell fragte den Namen deshalb immer
interaktiv ab, selbst wenn -BackupFile bereits mitgegeben war und der Name damit laengst im
Backup selbst steht. Invoke-sqmRestoreDatabase loest genau diesen Fall schon immer selbst (per
RESTORE HEADERONLY), Invoke-sqmRestoreTest hatte diese Angleichung nie bekommen.
Fix: -DatabaseName ist jetzt nur noch dann erforderlich, wenn -BackupFile FEHLT - dort dient
der Name als Suchschluessel fuer die msdb-Sicherungshistorie (Get-DbaDbBackupHistory) und es
gibt keinen anderen Weg, das passende Backup zu finden. Ist -BackupFile gegeben, wird der Name
bei Bedarf automatisch aus dem Backup-Header gelesen (RESTORE HEADERONLY), analog zu
Invoke-sqmRestoreDatabase. Fehlen sowohl -DatabaseName als auch -BackupFile, wird das jetzt
als klare Ablehnung (Status = 'Rejected') gemeldet, bevor ueberhaupt eine Verbindung aufgebaut
wird - statt PowerShell interaktiv nach dem Parameter fragen zu lassen.
Invoke-sqmRestoreDatabase protokollierte "Restore erfolgreich", ohne dass ueberhaupt restored wurdeVorfall auf SQL01 (AG AG01, Restore aus F:\Transfer\*.bak): das
Log zeigte fuer alle vier Laeufe (OrderDB, custom, SalesDB, OrderDB) durchgehend "Success" - inklusive
"Restore von ... erfolgreich" und "erfolgreich in AG aufgenommen". Eine Gegenprobe direkt in SQL
Server (msdb.dbo.restorehistory, letzter Eintrag pro Datenbank ueber ROW_NUMBER() OVER
(PARTITION BY d.Name ORDER BY r.restore_date DESC)) zeigte aber: das restore_date war eine
Woche alt - es hatte an diesem Tag ueberhaupt keinen tatsaechlichen RESTORE-Befehl gegeben. Die
Datenbanken blieben ausserdem im Modus RESTRICTED_USER stehen.
Drei zusammenhaengende Bugs im selben Codepfad, die sich gegenseitig verdeckt haben:
AG-Erkennung wirkungslos. Die Bedingung vor Remove-DbaAgDatabase pruefte
if (-not $agDbCheck) - eine Variable, die seit dem Refactoring in 1.9.27.0 (Abloesung durch
Get-sqmDatabaseAgMembership/$agMembership) nirgends mehr zugewiesen wurde und damit immer
$null war. Die Bedingung war folglich IMMER wahr, Remove-DbaAgDatabase lief nie - die
Datenbanken blieben aller Wahrscheinlichkeit nach die ganze Zeit ueber live Mitglied der AG.
Fix: $agDbCheck durch $agMembership.IsAgDatabase (die tatsaechlich zu Laufbeginn ermittelte
Live-Mitgliedschaft) ersetzt.
Der eigentliche RESTORE-Befehl wurde von SQL Server mutmasslich abgelehnt - mit exakt dem
seit 1.9.27.0 bekannten Fehler "RESTORE cannot operate on database ... because it is configured
for database mirroring or has joined an availability group", weil die Datenbank wegen Bug 1
noch in der AG steckte.
Diese Ablehnung wurde vom Code nie bemerkt, weil praktisch KEIN verschachtelter
dbatools-Aufruf in dieser Funktion -EnableException gesetzt hatte (nur -ErrorAction Stop).
dbatools-Funktionen (PSFramework Stop-Function) melden interne Ablehnungen ohne
-EnableException per Default nur als Warning und geben $null zurueck - OHNE eine
terminierende Exception zu werfen. -ErrorAction Stop allein greift dabei nicht, weil kein
reguraerer PowerShell-Fehlerdatensatz entsteht. Das eigene try/catch um Restore-DbaDatabase
sah dadurch nie einen Fehler, protokollierte "Restore erfolgreich" und liess $restoreSucceeded
auf $true stehen - obwohl $restoreResult in Wahrheit leer war. Alle nachfolgenden Schritte
(User-Import, Orphan-Repair, Owner setzen, AG-Rejoin) liefen anschliessend anstandslos gegen die
UNVERAENDERTE Alt-Datenbank durch, da sie selbst keine RESTORE-spezifische Sperre verletzen -
der Fehlschlag blieb dadurch komplett unsichtbar. Das RESTRICTED_USER blieb vermutlich Rest
eines frueheren, echten Restore-Versuchs (der Backup-Quelle in genau diesem Modus, siehe unten)
liegen und wurde nie zurueckgesetzt, weil ja nie ein neuer Restore lief, der das haette tun
koennen.
Fix: -EnableException an jeden verschachtelten dbatools-Aufruf ergaenzt, der bislang nur
-ErrorAction Stop gesetzt hatte (Restore-DbaDatabase, Export-DbaUser,
Repair-DbaDbOrphanUser, Set-DbaDbOwner, Remove-DbaAgDatabase, Remove-DbaDatabase,
Add-DbaAgDatabase, Set-DbaAgReplica, Get-DbaAgReplica, alle betroffenen
Invoke-DbaQuery-Aufrufe) - passend zum bereits etablierten Muster im Rest des Moduls (siehe
z.B. Get-sqmDatabaseAgMembership, Test-sqmBackupIntegrity). Connect-DbaInstance bewusst
ausgenommen (kennt laut fruehester Erfahrung in diesem Modul kein -EnableException, wirft aber
ohnehin echte Exceptions bei Verbindungsfehlern). Zusaetzlich wird das Rueckgabeobjekt von
Restore-DbaDatabase jetzt selbst geprueft (if (-not $restoreResult) { throw ... }) statt sich
allein darauf zu verlassen, dass nichts geworfen wurde.
Zusaetzlich, unabhaengig von den drei Bugs oben: nach einem tatsaechlich erfolgreichen Restore
wird jetzt immer der TATSAECHLICHE Live-Zustand (sys.databases.user_access_desc) abgefragt und
bei Abweichung von MULTI_USER zurueckgesetzt - vorher hing dieser Ruecksprung an der eigenen
$wasSingleUser-Fahne, die nur gesetzt wird, wenn die Funktion SELBST vor dem Restore
SINGLE_USER erzwungen hatte. RESTORE DATABASE uebernimmt aber den User-Access-Modus, der zum
Zeitpunkt der Datensicherung im Backup selbst galt (Boot-Page der Datenbank) - stand die
Quelldatenbank beim Sichern in RESTRICTED_USER (ueblich bei einer Migrations-Sicherung wie unter
F:\Transfer\), kaeme eine wiederhergestellte Kopie in genau diesem Modus wieder hoch,
unabhaengig davon, ob $wasSingleUser je $true war - und ein sysadmin-Login (unter dem diese
Funktion laut NOTES laufen muss) koennte sich trotzdem weiter verbinden, sodass der Zustand
unbemerkt bliebe.
Get-sqmADMemberGroups fand auf einer echten Kunden-Domaene (LDAP-Fallback) weiterhin 0 Gruppen, obwohl der Account nachweislich Mitglied warNach dem Fix in 1.9.33.0 (LDAP-Fallback lief jetzt ueberhaupt an) blieb das Ergebnis auf
BP.PROD.BANK.LBBW.SKO.DE trotzdem bei 0 - ohne jede Fehlermeldung. Gegenprobe mit
Get-sqmADGroupMembers (die andere Richtung: "wer ist Mitglied dieser Gruppe") fand den
getesteten Account anstandslos per LDAP als Mitglied einer bekannten Gruppe. Zwei Bugs in
Find-ParentGroups (LDAP-Zweig) gefunden und behoben:
(member=$memberDN)). Enthaelt der DN Zeichen, die im Filter anders escaped werden
muessen als im DN selbst - z.B. Kommas bei CN=Nachname, Vorname (Standard-
Namenskonvention in dieser Umgebung) - liefert die Suche lautlos 0 Treffer, keine
Exception. Fix: memberOf-Attribut direkt vom gefundenen User-Objekt lesen (derselbe
Rueckverweis-Mechanismus wie Get-ADPrincipalGroupMembership), kein DN-in-Filter mehr
noetig.InvokeGet("groupScope") aufgerufen - kein echtes
LDAP-Attribut (das AD-Schema kennt nur groupType; groupScope ist eine berechnete
Eigenschaft des PowerShell-AD-Moduls). Ein ungueltiger Attributname wirft eine Exception,
die den kompletten Gruppen-Datensatz (inklusive SamAccountName) aus der Ergebnisliste
riss, bevor er hinzugefuegt wurde - selbst wenn die Suche selbst Treffer fand. Fix: jedes
Attribut einzeln tolerant lesen (gleiches Muster wie bereits in
Get-sqmADGroupMembersRecursive verwendet), groupType statt groupScope.Get-sqmADMemberGroups und Get-sqmADGroupMembersRecursive lieferten auf Servern ohne ActiveDirectory-Modul stumm 0 ErgebnisseAuf einer echten Kunden-Domaene (BP.PROD.BANK.LBBW.SKO.DE) meldeten beide Funktionen
"0 Groups"/"0 Members" ohne jede Fehlermeldung, auch nicht im Log. Ursache: der LDAP-Fallback
steckte im catch-Block des ActiveDirectory-Modul-Pfads:
try {
if (Get-Module -ListAvailable -Name ActiveDirectory) { ... AD-Cmdlet-Pfad ... }
}
catch { ... LDAP-Fallback ... }
Ist das ActiveDirectory-Modul (RSAT) auf dem Zielserver schlicht nicht installiert, ist die
if-Bedingung einfach $false - das wirft keine Exception, der catch-Block (und damit
der komplette LDAP-Fallback) lief also nie an. Reproduziert im Log: nach der korrekt
aufgeloesten Domain kam sofort "0 Groups found", ohne einen einzigen Eintrag dazwischen.
Betraf Get-sqmADMemberGroups (Find-ParentGroups) und Get-sqmADGroupMembersRecursive
(Expand-GroupMembers) identisch. Get-sqmADGroupMembers war nicht betroffen - dort war der
Fallback bereits ueber ein explizites $methodUsed-Flag verdrahtet, nicht ueber catch.
Fix: beide Helper-Funktionen tragen jetzt dasselbe $usedAdModule-Flag-Muster. Der
LDAP-Fallback laeuft jetzt sowohl wenn das AD-Modul fehlt als auch wenn es eine Exception
wirft. Zusaetzlich werden beide Fehlerfaelle jetzt explizit als WARNING geloggt statt komplett
stumm zu bleiben.
Get-sqmAgentJobScheduleReport oeffnete den HTML-Bericht nicht automatischAlle anderen Report-Funktionen des Moduls unterstuetzen den Standard-Mechanismus zum
automatischen Oeffnen des erzeugten HTML-Berichts (-NoOpen-Switch plus Aufruf von
Invoke-sqmOpenReport nach dem Schreiben der Datei). Get-sqmAgentJobScheduleReport hatte
diesen Baustein noch nicht - der Bericht wurde erzeugt, aber nie geoeffnet. Jetzt konsistent
mit z.B. Get-sqmDeadlockReport und Get-sqmWaitStatistics.
Get-sqmAgentJobScheduleReport hatte drei nachweisbare Fehler in der Statusermittlung, alle gegen
eine echte SQL Server 2022-Instanz reproduziert:
Ein Job, der noch nie gelaufen ist (oder dessen Historie vollstaendig purgiert wurde),
wurde als "Failed" gemeldet statt als "noch nie gelaufen". Die T-SQL-Anweisung
CASE WHEN jh.run_status = 1 THEN 'Success' ELSE 'Failed' END faellt bei run_status = NULL
(kein Historieneintrag vorhanden) in den ELSE-Zweig, weil NULL = 1 in SQL weder wahr noch
falsch, sondern UNKNOWN ist. Reproduziert: ein frisch angelegter, noch nie gestarteter Job
lieferte LastRunStatus = Failed.
MAX() auf die Ergebnis-Strings 'Success'/'Failed' aggregierte alphabetisch, nicht
zeitlich - 'Success' > 'Failed'. Ein Job, dessen tatsaechlich LETZTER Lauf fehlgeschlagen
ist, aber irgendwann vorher einmal erfolgreich war, wurde als 'Success' gemeldet.
Reproduziert: Lauf 1 erfolgreich, Lauf 2 (der spaetere) mit absichtlichem Fehler - die alte
Abfrage lieferte weiterhin Success.
Ein Job mit zwei oder mehr Zeitplaenen wurde durch den JOIN auf sysjobschedules und
sysschedules vervielfacht (2 Zeitplaene x N Historienzeilen), wodurch die GROUP BY-Abfrage
mehrere Zeilen statt einer je Job lieferte. $jobHistoryData | Where-Object JobName -eq ...
ergab dadurch ein Array statt eines einzelnen Datensatzes, und Felder wie LastRunDate und
LastRunStatus wurden faelschlich als Arrays weiterverarbeitet. Reproduziert: LastExecution
zeigte "Never" (die [string]-Konvertierung eines Arrays mit zwei Datumswerten ergibt einen
durch Leerzeichen getrennten String, den TryParse als Ganzzahl ablehnt), Schedule zeigte
"No Schedule" trotz zweier vorhandener Zeitplaene, LastStatus wurde zu {Success, Success}
statt eines einzelnen Werts.
Die Abfrage trennt jetzt beide Fragen technisch voneinander: eine CTE mit
ROW_NUMBER() OVER (PARTITION BY job_id ORDER BY instance_id DESC) liefert genau eine Zeile je
Job - den tatsaechlich letzten Lauf. instance_id ist SQL Agents eigener, garantiert monoton
steigender Zaehler und damit zuverlaessiger als ein Vergleich von run_date/run_time. Die
Zeitplaene werden separat auf einen reprsentativen Zeitplan je Job reduziert (samt Gesamtzahl,
falls mehrere existieren, sichtbar als "(+1 weitere(r) Zeitplan/Zeitplaene)"), statt die Historie
zu vervielfachen. Ein Job ohne Historie bekommt jetzt den eigenen Status Never Run statt
Failed - er zaehlt in der Zusammenfassung weder als Erfolg noch als Fehlschlag.
Getestet gegen SQL Server 2022 auf DEV01 mit drei praeparierten Faellen (nie gelaufen, ein
Zeitplan mit Erfolg, zwei Zeitplaene, sowie ein Job mit Erfolg gefolgt von einem tatsaechlichen
Fehlschlag) - alle drei zeigen jetzt den korrekten Status. Dazu fuenf neue Pester-Tests
(tests/Unit/Public/Get-sqmAgentJobScheduleReport.Tests.ps1), die die Statusableitung ohne
Serverzugriff nachbilden; der entscheidende Test wurde per Mutation gegengeprueft (mit dem alten
Verhalten wieder eingebaut schlaegt er fehl). Volle Suite: 195 Tests, keine Fehlschlaege.
Die Datenbanktabelle in Invoke-sqmSetupReport hat eine Spalte "Datenbank-Modus" bekommen, und der
SERVER-Abschnitt weist den Standard der Instanz aus. Ausgegeben wird die SQL Server-Version, fuer
die der Grad steht, nicht nur die nackte Zahl: 2019 (150) statt 150. Die Zahl allein sagt nur
denen etwas, die die Tabelle auswendig koennen, dabei entscheidet genau dieser Wert darueber,
welches Verhalten und welcher Abfrageoptimierer fuer die Datenbank tatsaechlich gelten.
Abgedeckt sind 2000 bis 2025 (80 bis 170). Liegt eine Datenbank unter dem Stand ihrer Instanz, wird das ausdruecklich vermerkt, denn genau diese Konstellation faellt sonst niemandem auf:
sqmCompatTest Full 2019 (150), niedriger als die Instanz: 2022 (160)
Der Standard der Instanz wird aus dem Kompatibilitaetsgrad von model gelesen, also aus dem Wert,
den neu angelegte Datenbanken erhalten.
Invoke-sqmSplunkConfiguration -Mode Test wurde ohne Zielangabe aufgerufen und prueft damit den
Rechner, auf dem der Bericht ERZEUGT wird. Bei einem Bericht ueber eine entfernte Instanz stand im
Ergebnis also der Zustand einer voellig anderen Maschine, ohne dass das erkennbar war - im
Protokoll sichtbar als "=== DEV03 ===" waehrend ueber DEV01 berichtet wurde.
Laeuft die berichtete Instanz auf dem lokalen Rechner, wird weiterhin lokal geprueft. Sonst wird der
Zielrechner ueber -ComputerList adressiert. Zusaetzlich wird das Ergebnis jetzt streng bewertet:
schlaegt die Remoteverbindung fehl, liefert die Funktion trotzdem ein Objekt mit
IsConfigured = $false zurueck. Das allein auszuwerten hiesse, "nicht erreichbar" als "nicht
konfiguriert" zu melden. Gemeldet wird deshalb nur dann ein Zustand, wenn die Antwort auch
nachweislich vom gefragten Rechner kommt und einen auswertbaren Status hat, sonst
"Nicht ermittelbar" mit Grund.
Vollstaendig ueber die SQL-Verbindung, ohne WinRM, und damit garantiert von der berichteten Instanz:
sys.dm_os_cluster_nodes samt Statussys.dm_hadr_cluster, dazu alle Clustermitglieder aus
sys.dm_hadr_cluster_members mit Typ, Zustand und Stimmenzahl - auch die Knoten, auf denen diese
Instanz gar nicht laeuftDamit beantwortet der Bericht auch die Frage nach weiteren Knoten, ohne dass man sich dafuer auf einen der beteiligten Server verbinden muss.
Getestet gegen SQL 2022 auf DEV01: Einzelinstanz ohne AlwaysOn wird korrekt als solche ausgewiesen, der Splunk-Abschnitt meldet nicht mehr faelschlich den lokalen Rechner. Der Cluster- und AG-Zweig selbst konnte mangels verfuegbarem Cluster nicht durchgespielt werden.
Die Kachel "Backup Jobs" in Invoke-sqmSetupReport bewertete ausschliesslich SQL Agent-Jobs, deren
Name backup oder bkp enthaelt. Wird ueber ein externes Werkzeug gesichert - TDP/TSM, ein
zentraler Backupserver, ein Scheduler ausserhalb des SQL Servers - gibt es solche Jobs nicht, und
der Bericht meldete NO BACKUP JOBS, obwohl jede Nacht sauber gesichert wurde.
Die Bewertung kommt jetzt aus msdb.dbo.backupset, also aus der tatsaechlichen Sicherungshistorie.
Dort steht jede Sicherung, unabhaengig davon, wer sie ausgeloest hat, auch die ueber Virtual Device
Interface laufenden Sicherungen von TDP/TSM. Gemeldet wird, wie viele Datenbanken ueberhaupt keine
Vollsicherung haben (rot, mit Namen) beziehungsweise aelter als sieben Tage sind (orange), sonst
gruen mit dem Alter der aeltesten Sicherung. Die Agent-Jobs stehen weiterhin als Zusatzinformation
dabei, nur nicht mehr als Bewertungsgrundlage.
Der Fehler ging in beide Richtungen: bei der Pruefung gegen eine Testinstanz meldete die alte Logik "OK (5 jobs)", waehrend 11 von 20 Datenbanken tatsaechlich nie gesichert worden waren.
Die Dienstkonten wurden ueber Get-Service und Get-CimInstance Win32_Service ohne
-ComputerName gelesen. Beides fragt damit den Rechner ab, auf dem das Skript laeuft: bei einem
Bericht ueber eine entfernte Instanz wurde also das Konto des lokalen Rechners ausgewiesen. Auf
einem Host mit mehreren Instanzen kam durch Select-Object -First 1 zusaetzlich eine beliebige
davon heraus.
Gelesen wird jetzt sys.dm_server_services. Die DMV liefert genau die Dienste der verbundenen
Instanz, ohne WinRM, samt Status und Starttyp.
Abschnitt SERVER: Collation, Edition und Produktversion, Anzahl logischer Prozessoren nebst
Sockelzahl und Scheduler, sowie der OS-Arbeitsspeicher. Alles aus SERVERPROPERTY und
sys.dm_os_sys_info, also ueber die SQL-Verbindung und damit unabhaengig von WinRM.
Abschnitt INSTALLIERTE KOMPONENTEN: Volltextsuche (SQL-seitig sicher feststellbar), Integration
Services, Analysis Services und Reporting Services sowie der Monitoring-Registry-Schluessel aus
Invoke-sqmMonitoringKey. Die letzten vier sind Hostangaben und brauchen WinRM. Ist das nicht
moeglich, steht dort ausdruecklich "nicht ermittelbar" samt Grund, nicht "nicht installiert" -
fuer SSIS und SSRS wird ersatzweise geprueft, ob ein SSISDB- beziehungsweise ReportServer-Katalog
auf dieser Instanz liegt, was ebenfalls als Indiz und nicht als Nachweis benannt wird.
Getestet gegen SQL 2022 auf DEV01 (Windows, ohne WinRM-Zugriff, also der Fallback-Pfad) und
gegen SQL 2022 unter Linux im Container. Dienstkonten kommen nachweislich von der Zielinstanz
(NT Service\MSSQLSERVER), nicht mehr vom ausfuehrenden Rechner.
Invoke-sqmRestoreDatabase ist beim User-Export gescheitert und hat den Lauf danach beendet, ohne
dass ein Restore stattgefunden hat:
UserExport Failed Fehler beim Export der User: Es wurde kein Parameter gefunden,
der dem Parameternamen "Confirm" entspricht.
Ursache war 1.9.26.0 ("pass -Confirm:$false to every nested dbatools cmdlet explicitly"): der
Parameter wurde pauschal an jeden genesteten dbatools-Aufruf gehaengt. Export-DbaUser unterstuetzt
ShouldProcess aber nicht (geprueft gegen dbatools 2.8.2), und der Aufruf schlaegt damit sofort mit
einer ParameterBindingException fehl. Da der catch des User-Exports den Lauf per return
beendet, war der Restore als Ganzes blockiert - die Meldung liess das nur nicht erkennen.
-Confirm:$false entfaellt an dieser Stelle. Unterdrueckte Rueckfragen laufen jetzt zusaetzlich
zentral ueber $ConfirmPreference = 'None' im begin-Block, das wirkt auf jedes Cmdlet unabhaengig
davon, ob es den Parameter ueberhaupt kennt, und ist gegen kuenftige dbatools-Versionen robust. Die
uebrigen acht Aufrufstellen wurden geprueft, dort ist -Confirm jeweils vorhanden
(Remove-DbaAgDatabase, Remove-DbaDatabase, Repair-DbaDbOrphanUser, Set-DbaDbOwner,
Set-DbaAgReplica, Add-DbaAgDatabase, Set-sqmSqlPolicyState).
Ein neuer Test scannt die Datei per AST und meldet jeden Befehl, der -Confirm erhaelt, obwohl das
aufgeloeste Cmdlet den Parameter nicht kennt. Per Mutation gegengeprueft: mit dem alten Aufruf
wieder eingebaut schlaegt er fehl.
Getestet gegen eine echte Instanz (DEV01, SQL 2022, ohne AlwaysOn), unter Windows PowerShell
5.1: Backup mit definiertem Inhalt gezogen, Datenbank danach veraendert, Invoke-sqmRestoreDatabase
ausgefuehrt, Inhalt exakt auf den Backup-Stand zurueck, acht Schritte ohne Fehlschlag
(UserExport, RestoreStep, UserImport, FixOrphans, RemoveOrphanWindowsLogins, SetDbOwner),
Datenbank danach ONLINE und MULTI_USER.
Invoke-sqmRestoreDatabase ist bei einer Datenbank, die nachweislich Mitglied einer Availability
Group war, in den Standalone-Pfad gelaufen. Es hat das Entfernen aus der AG uebersprungen und ist
erst mehrere Schritte spaeter an den Operationen gescheitert, die SQL Server bei einer AG-Datenbank
grundsaetzlich ablehnt:
[Invoke-DbaQuery] The operation cannot be performed on database "OrderDB" because it is involved in
a database mirroring session or an availability group. ALTER DATABASE statement failed.
[Restore-DbaDatabase] RESTORE cannot operate on database 'OrderDB' because it is configured for
database mirroring or has joined an availability group.
Ursache war die Erkennung selbst:
$agDbCheck = Get-DbaAgDatabase -SqlInstance $SqlInstance -Database $finalDbName -ErrorAction SilentlyContinue
if ($agDbCheck) { ... }
Fehlende Rechte (VIEW ANY DEFINITION), eine stolpernde SMO-Enumeration oder eine Verbindung zu
einer anderen Instanz als gemeint liefern alle dasselbe leere Ergebnis wie eine echte
Standalone-Datenbank. Der unterdrueckte Fehler machte aus "konnte ich nicht feststellen"
stillschweigend ein "ist in keiner AG" - und ausgerechnet daran haengt der Schritt, der den Restore
ueberhaupt erst moeglich macht.
Neu ist die private Hilfsfunktion Get-sqmDatabaseAgMembership. Sie fragt direkt die
Katalogsichten (sys.availability_databases_cluster fuer die Mitgliedschaft, clusterweit von jedem
Replikat lesbar, und sys.dm_hadr_availability_replica_states fuer die aktuelle Primary) und
beantwortet drei Zustaende statt zwei: AlwaysOn nicht aktiviert, aktiviert aber kein Mitglied, und
nicht ermittelbar. Der dritte Fall wirft jetzt, statt den Lauf auf einer falschen Annahme
fortzusetzen. Steht die Mitgliedschaft per Katalogsicht fest, laesst sich das SMO-AG-Objekt aber
nicht laden, bricht die Funktion ebenfalls ab, statt die Datenbank am Ende standalone
zurueckzulassen.
War AvailabilityGroup.PrimaryReplicaServerName leer (etwa waehrend eines Failovers), fiel die
Funktion auf die verbundene Instanz zurueck. Ist das ein Sekundaerreplikat, scheitern Restore und
ALTER DATABASE dort aus demselben Grund erneut. Jetzt wird zuerst die per DMV ermittelte Primary
verwendet und erst danach, mit deutlicher Warnung, die verbundene Instanz.
Die Pruefung "hat die Instanz genau eine AG, der die Datenbank beitreten soll" rief
Get-DbaAvailabilityGroup auch dann auf, wenn AlwaysOn auf der Instanz gar nicht aktiviert ist.
Das erzeugte bei jedem Restore auf einer Einzelinstanz die Warnung "Availability Group (HADR) is
not configured for the instance", die im Betrieb wie ein Problem aussieht, aber der Normalzustand
ist. Der Aufruf entfaellt jetzt in diesem Fall.
Getestet: Erkennung auf einer Instanz ohne AlwaysOn (sauberes "nein", kein Abbruch), Fehlerfall
auf einer nicht erreichbaren Instanz (Ausnahme statt stillem "nein"), beide Katalogabfragen gegen
SQL 2022, sowie der vollstaendige Standalone-Pfad von Invoke-sqmRestoreDatabase bis zum
Restore-Schritt. Der AG-Pfad selbst liess sich mangels verfuegbarer Availability Group nicht
durchspielen.
Dazu acht neue Pester-Tests in tests/Unit/Public/Invoke-sqmRestoreDatabase.Tests.ps1, die alle
sechs Zustaende der Erkennung abdecken und festhalten, dass der Lauf abbricht, BEVOR User-Export
oder Restore anlaufen, wenn die AG-Zugehoerigkeit nicht ermittelbar ist. Der entscheidende Test
wurde per Mutation gegengeprueft: mit dem alten stillen Fallback wieder eingebaut schlaegt er fehl.
Die vollstaendige Suite laeuft mit 189 Tests ohne Fehlschlag durch.
Elf Aufrufstellen in fuenf Funktionen splatteten eine Hashtable, die bereits
ErrorAction = 'Stop' trug, und setzten am selben Aufruf zusaetzlich ein explizites
-ErrorAction. Windows PowerShell 5.1 lehnt das mit ParameterBindingException
(ParameterAlreadyBound) ab, PowerShell 7 toleriert es stillschweigend, der Fehler ueberlebt
also jeden Test unter PS 7 und schlaegt erst auf der Zielplattform zu, die das Manifest mit
PowerShellVersion = '5.1' deklariert. Derselbe Fehler hatte in sqmDataTransfer 0.1.17.1
jeden regulaeren Resume-Lauf abgebrochen.
Betroffen waren Set-sqmSsrsConfiguration (4), Set-sqmSsrsHttpsCertificate (4),
Install-sqmSsrsReportServer (1), Copy-sqmLogins (1) und Get-sqmLongRunningQueries (1).
Bei den drei SSRS-Funktionen existierte die Splat-Hashtable praktisch nur, um ErrorAction zu
transportieren; dort ist der Schluessel entfallen, das explizite -ErrorAction je Aufruf bleibt.
Bei Copy-sqmLogins und Get-sqmLongRunningQueries wird umgekehrt der redundante explizite
Parameter weggelassen, weil dort weitere Aufrufstellen auf dem Hashtable-Wert aufsetzen.
Am gravierendsten war Copy-sqmLogins: die Ermittlung des sa-Kontos ueber die well-known SID
0x01, die dafuer sorgt, dass auch ein umbenanntes sa vom Login-Abgleich ausgeschlossen
wird, stand in einem try mit leerem catch { }. Unter PowerShell 5.1 scheiterte die Bindung,
der leere catch verschluckte den Fehler, und ein umbenanntes sa wurde ohne jede Meldung in den
Abgleich aufgenommen.
isset in Install-sqmSsrsReportServer war kein PowerShellDie Aufraeumzeile der SSRS-Vorhandenseinspruefung lautete if (isset $checkSession). isset
ist PHP und im Modul nirgends definiert, der Aufruf warf also zwingend. Da er im selben try
stand wie die Pruefung selbst, landete jeder Lauf im catch und das Ergebnis war immer
"SSRS ist nicht installiert", unter PowerShell 7 wegen isset, unter 5.1 schon eine Zeile
frueher wegen der doppelten Bindung. Korrigiert zu if ($checkSession).
Beide oben genannten Fehler konnten nur deshalb unbemerkt bleiben, weil ein leerer bzw.
pauschaler catch sie in ein harmlos aussehendes Ergebnis verwandelt hat. Die sa-Erkennung in
Copy-sqmLogins und die Vorhandenseinspruefung in Install-sqmSsrsReportServer setzen im
Fehlerfall jetzt eine WARNING ab, die benennt, welche Annahme stattdessen gilt. Das Verhalten
selbst bleibt unveraendert.
AST-basierte Pruefung, die genau dieses Muster modulweit findet, inklusive nachtraeglicher
Index-Zuweisungen ($p['ErrorAction'] = 'Stop'), aller Common Parameter und der
Parameter-Abkuerzungen (-ea, -wa). Haengt im Pre-Push-Hook und als eigener Schritt in der CI.
Ein reiner Import-Test findet diese Fehlerklasse nicht, da sie erst zur Aufrufzeit auftritt.
DiskFreeSpaceThresholdPct wurde ausschliesslich in einem kundenspezifischen Konfigurationsblock gesetzt und fehlte in den neutralen Standardwerten, obwohl Set-sqmConfig den Schluessel als Parameter anbietet. Fuer alle Nutzer ausserhalb dieser Umgebung (also jeden, der ueber die PSGallery installiert) existierte er damit gar nicht im Config-Store: Get-sqmConfig -Key 'DiskFreeSpaceThresholdPct' lief in den Nicht-gefunden-Zweig, warnte und lieferte $null, was [int]$null zu 0 machte.
Get-sqmDiskInfoByDriveLetter war betroffen: mit Schwellwert 0 ist $freePercent -lt 0 nie wahr, $extendNeededGB blieb also fuer jedes Laufwerk auf 0. Die Berechnung "wie viele GB muss erweitert werden" war ausserhalb dieser kundenspezifischen Umgebung still wirkungslos.Get-sqmServerHardwareReport fing den Wert bereits per if ($diskThreshold -le 0) { 10 } ab (zwei Stellen) und war inhaltlich korrekt, setzte aber die ueberfluessige Warnung ab.Der Schluessel steht jetzt mit Default 10 in den neutralen Standardwerten, analog zu den uebrigen Check*-Grenzwerten. Der Config-Store hat damit ausserhalb dieser Umgebung 32 statt 31 Schluessel.
BEHAVIOUR CHANGE: Die Idle-Liste stammte aus einer Zeit vor SQL 2016 und kannte weder SOS_WORK_DISPATCHER (2019+) noch QDS_* (2016+), PREEMPTIVE_XE_DISPATCHER oder MEMORY_ALLOCATION_EXT. Trotz IncludeIdle=False landeten die im Report, und weil sie dauerhaft im Leerlauf mitlaufen, dominieren sie jede Summe. Auf einer echten 2022er Kundeninstanz (Top 25, 66.845.984 Sek. gesamt):
SOS_WORK_DISPATCHER allein 88,3 % der ausgewiesenen Wartezeit, dazu QDS_* mit 3,4 %.WaitTimePct wertlos: der eigentliche Befund, CXCONSUMER/CXPACKET/CXSYNC_PORT, erschien als harmlose 6,6 %.Die Liste folgt jetzt der etablierten SQLskills-Ignore-Liste (Paul Randal), inklusive der Typen aus 2016/2017/2019 (QDS_*, SOS_WORK_DISPATCHER, PARALLEL_REDO_*, PVS_PREALLOCATE, PREEMPTIVE_XE_*). Auf demselben Datensatz bleiben statt 66.845.984 noch 4.945.737 Sek. echte Wartezeit uebrig, und Parallelismus steht mit 88,6 % da, wo er hingehoert.
Die Idle-Typen behalten ihre Kategorie und Empfehlung, mit -IncludeIdle sind sie weiterhin sinnvoll beschriftet, nur eben nicht mehr im Default-Report.
CXSYNC_PORT wird als Parallelism gefuehrt statt als Other; mit 17,8 % der bereinigten Wartezeit gehoert es zum Befund und nicht in die Restkategorie.
BEHAVIOUR CHANGE: Die Recommendation-Spalte bewertete kumulierte Summen. Die wachsen aber allein mit der Uptime, nicht mit dem Problem. Auf derselben Instanz:
PAGEIOLATCH_SH mit 2,05 ms Durchschnitt -> "Disk I/O bottleneck". 2 ms ist gesundes Storage.SOS_SCHEDULER_YIELD mit 0,12 ms Durchschnitt -> "CPU pressure". Der instanzweite Signal-Wait-Anteil lag bei 2,7 %, also weit weg von CPU-Druck.Empfehlungen haengen jetzt an Schwellwerten, je nach dem, was den Wait-Typ tatsaechlich verraet:
MinAvgWaitMs, Durchschnittsdauer, fuer I/O, Locks, Latches, Netzwerk. PAGEIOLATCH_* ab 10 ms, WRITELOG ab 5 ms, LCK_M_* ab 500 ms.MinWaitPct, Anteil an der bereinigten Wartezeit, fuer Parallelismus und Memory. Dort sind viele kurze Waits normal; weh tun sie erst in der Masse. Eine Durchschnittsschwelle haette hier genau den echten Befund verschluckt (CXPACKET liegt bei 2,31 ms).MinSignalWaitPct, instanzweiter Signal-Wait-Anteil, fuer SOS_SCHEDULER_YIELD. Das ist das etablierte Mass fuer CPU-Druck; die absolute Summe sieht auf jeder Instanz mit genug Uptime gross aus.THREADPOOL ist nie harmlos.Unterschreitet ein Wait seinen Schwellwert, bleibt die Zelle nicht leer, sondern nennt den gemessenen Wert und die Schwelle ("Unauffaellig: Durchschnitt 2,05 ms liegt unter dem Schwellwert von 10 ms"). Sonst raet der Leser, ob "unauffaellig" oder "nicht bewertet" gemeint ist.
Neue Spalte SignalWaitPct je Wait, damit der Report die CPU-Bewertung selbst belegt. Der instanzweite Anteil steht im Log.
Login-zentrierter Berechtigungsreport: Serverrollen und explizite Serverrechte, dazu je Datenbank der zugeordnete User mit seinen Datenbankrollen und GRANT/DENY-Rechten. Eine flache Zeile je Berechtigung, damit sich das Ergebnis filtern und nach CSV/HTML exportieren laesst.
sqmTestLogin der User sqmTestUser. Ein Namensabgleich haette diese Berechtigung schlicht nicht gefunden.-IncludeOrphanedUsers als verwaist ausgewiesen (IsOrphaned), inklusive ihrer Rechte, sonst verschwindet genau der Fall, der sicherheitlich interessant ist.COLLATE DATABASE_DEFAULT. Ohne das bricht die Abfrage mit einem Sortierungskonflikt ab, weil sys.database_principals.name (sysname, Latin1_General_CI_AS_KS_WS) und permission_name (Latin1_General_CI_AS) unterschiedlich sortieren. Auf der Testinstanz war das reproduzierbar.SQL Server speichert einen "letzten Login" nirgends persistent. Die Funktion sammelt deshalb nur, was die Instanz wirklich belegen kann, und liefert zu jedem Wert Source und Confidence mit:
Live, sys.dm_exec_sessions, also nur bestehende Sessions. Jeder Dienstneustart setzt das zurueck, weiter als sqlserver_start_time kann die Quelle nie zurueckblicken. Der Wert wird als CoverageSince mitgegeben, damit ein leeres Ergebnis einordenbar bleibt.ErrorLog, erfolgreiche Anmeldungen im Fehlerprotokoll. Nur verfuegbar bei AuditLevel 1 oder 3; der Default 2 protokolliert ausschliesslich Fehlversuche und liefert hier nichts. Wird erkannt und gemeldet, statt stillschweigend leer zu bleiben.LastAccess = $null, Confidence = 'Unknown' plus Note. Das heisst "nicht belegbar", nicht "nie benutzt", die Unterscheidung wird bewusst nicht eingeebnet.Der ErrorLog-Parser ist sprachneutral: die Vorlagen fuer 18453/18454 kommen aus sys.messages der Instanz und werden zur Regex gebaut (ConvertTo-sqmMessageRegex, privat). Noetig, weil die Meldungen lokalisiert sind, Englisch quotet einfach (user 'x'), Deutsch doppelt (Benutzer "x"), und die Platzhalter unterscheiden sich (%.*ls vs %1!). Eine fest verdrahtete Regex haette auf jeder deutschen Instanz still nichts gefunden. Gegen die echten Vorlagen in Englisch, Deutsch und Franzoesisch geprueft, inkl. Namen mit Backslash und Apostroph; die Fehlermeldung 18456 matcht die Erfolgs-Regex nachweislich nicht.
DaysSince rechnet gegen GETDATE() der Instanz, nicht gegen die Uhr des Clients, bei Zeitversatz kamen sonst negative Werte heraus (auf der Testinstanz reproduziert).
Windows-Gruppenlogins bekommen einen Hinweis: Mitglieder verbinden sich mit ihrem eigenen AD-Konto, die Session wird nie der Gruppe zugeordnet. Ein leerer Wert ist dort erwartbar.
BEHAVIOUR CHANGE: Die Abfrage lieferte NULL AS LastLogin, eine feste Konstante, keine Spalte. Damit war die Inaktivitaetspruefung tot und jeder Login fiel in den elseif-Zweig. Folge:
SQL_LOGIN filterte. Ein seit Jahren toter Domaenen-Account blieb unauffaellig.Neu: der letzte Zugriff kommt aus Get-sqmLoginLastAccess. Inactive wird nur noch gemeldet, wenn der Zugriff belegt und aelter als die Schwelle ist, mit Datum und Quelle im Befund. Der Befundtyp NeverUsed entfaellt ersatzlos; er war nicht belegbar.
Reicht die Datenlage grundsaetzlich nicht, meldet der Report das einmal pro Instanz (NoAccessTracking) statt pro Login. Ein Report, der auf einer Standardinstanz jeden Login anmeckert, uebertoent jeden echten Befund, die Luecke gehoert zur Instanz, nicht zum Login. Neuer Schalter -SkipAccessCheck fuer Konten ohne VIEW SERVER STATE / xp_readerrorlog.
HHmsqm-Rest in Private\Test-sqmLoggingPath trug die Formatzeichenfolge yyyyMMddHHmsqmfff noch. Der Sweep in 1.9.24.1 hatte nur Public\ geprueft, deshalb blieb sie stehen. Auswirkung gering, der Name enthaelt zusaetzlich Get-Random, und die Datei wird sofort wieder geloescht, , aber es ist derselbe Defekt.
yyyyMMdd_HHmsqm appeared in 21 places across 19 functions in Public\, the residue of a search/replace for the sqm module prefix that ate the mss of HHmmss. It is a legal format string, so nothing ever threw: m is minutes without zero-padding, s is seconds without zero-padding, q is not a format specifier and is emitted literally. 18:12:05 became 1812q1 instead of 181205, seconds effectively lost, minutes unpadded, a stray q in the filename. All 21 now read yyyyMMdd_HHmmss, matching the sibling functions that were never corrupted.Invoke-sqmRestoreDatabase (pre-restore safety backup) and Invoke-sqmUserDatabaseBackup used the broken stamp for .bak FILENAMES. Because the format collapses HHmmss to a value that no longer distinguishes seconds, two backups of the same database inside the same minute produced an identical filename and the second silently overwrote the first, including the safety backup taken immediately before a restore.Invoke-sqmQueryStore additionally wrote both of its reports to the wrong place. Lines 603/641 interpolated "$baseFile_TopQueries.csv" and "$baseFile_Issues.txt"; an underscore is a legal character in a PowerShell variable name, so these parsed as the single, undefined variables $baseFile_TopQueries / $baseFile_Issues, expanded to empty, and the paths became literally .csv and .txt in the process's current directory. $OutputPath was ignored (the directory was created and left empty) and every database in the loop overwrote the previous one's file. Now "${baseFile}_...", the brace form already used on the adjacent line 598.gespeichert: .csv all along.HHmsqm remains under Public\; no bare $var_suffix interpolation remains; all files in Public\ parse without errors and retain their UTF-8 BOM.BEHAVIOUR CHANGE: the evidence report used to be German unconditionally. It now follows the module configuration key Language, whose default is en-US, so after this update the report is English by default. German output: Set-sqmConfig -Language de-DE (once per machine; the value is persisted). Everything else is unaffected, only this function's TXT/HTML evidence is localized.
Get-sqmString, _s, Private\Strings\de-DE.psd1 and en-US.psd1 have shipped for a while but were used by 0 of 153 functions; the Language default of en-US was therefore never exercised. Restore-test evidence is the natural first candidate: it is the output most likely to be handed to an international auditor.RestoreTest_*). Verified that both files carry all keys and that every key used in code exists in both."{0:N2}" against the CURRENT culture, so on a German Windows an English report would have read "213,08 MB", which an English reader parses as 213 THOUSAND. The thread culture is now set to the report language around the formatting and restored in a finally, so a failure cannot leave the session's culture altered. Verified: en-US gives "213.08 MB", de-DE gives "213,08 MB", and the session culture is unchanged afterwards.[long]$row.BackupSize.Byte, guarded only by if ($row.BackupSize). That guard passes for ANY non-null value, but .Byte only exists on the dbatools Size object. Where dbatools hands back a plain numeric value (or $null) instead - version- and code-path-dependent - .Byte resolves to $null, [long]$null is 0, and the measurement silently became 0 B. A restore test that documents "0 B" is worthless as evidence, and it failed silently: Status still said Success.ConvertTo-sqmSizeBytes now handles all shapes: Size object (.Byte), plain numeric value, numeric string, $null, and anything unusable. Verified against each.SizeSource (RestoreResult, BackupHeader, Unknown) so the origin of the number is visible.<OutputPath>\RestoreTest (default C:\System\WinSrvLog\MSSQL\RestoreTest), using the module's standard report helpers (ConvertTo-sqmHtmlReport, Get-sqmReportReference, Invoke-sqmOpenReport).RestoreTest_ (rejected before a connection is even opened), and must differ from the source database name.-AllowReplaceExistingTestDatabase is given; only then is WITH REPLACE used, and only for a RestoreTest_-prefixed name.-ReplaceDbNameInFile renames the physical files, so the restore can never write over the source database's data files.RestoreTest_ prefix is a code constant, not a config key - a settable guard is no guard.-RemoveTestDatabase drop re-checks the prefix independently.-RemoveTestDatabase cleans up.DatabaseRestoreTime: that value only has whole-second resolution. Verified on DEV01 - SQL reported 00:00:01 for a restore that actually took 2.42s, which would have inflated the documented throughput from a true 94 MB/s to 228 MB/s. The SQL-reported value is still carried in the result object as SqlReportedRestoreTime for reference.RestoreTestRetentionMonths (default 12), overridable per run via -RetentionMonths; 0 keeps evidence forever. The cleanup only ever touches files matching this function's own naming pattern (RestoreTest_* AND extension .txt/.html), so pointing -OutputPath at a shared directory cannot delete unrelated files. It runs after the current evidence has been written, so a failing cleanup cannot cost the new report.-BackupFile is now OPTIONAL. Without it the newest FULL backup is looked up automatically: msdb backup history first (Get-DbaDbBackupHistory -LastFull), directory scan as fallback. -IncludeChain restores the whole chain (last full + diff + subsequent logs) instead.-BackupFile from the generated wrapper unless one was explicitly given, so every run resolves the current backup itself.<DB>\FULL\ - i.e. WITHOUT the server-name level Ola's default would produce - which is exactly why a scan with hardcoded assumptions is the wrong primary source._FULL_yyyyMMdd_HHmmss scheme lexicographic order equals chronological order. Files are grouped by that timestamp so a striped backup returns all its parts.-IncludeChain only works via msdb: the LSN relationship cannot be read off file names, so the scan can only ever return a full backup. It logs a warning and degrades to full-only rather than silently pretending a chain was tested.BackupSource: BackupHistory / DirectoryScan / Parameter) - an auditor must be able to see which backup a measurement refers to.-Verbose -ContinueOnError - parameters Invoke-sqmRestoreTest does not have - and quotes with double quotes, interpolating $ in paths).-ScheduleType Weekly/Daily, -NoSchedule for manual starts. -ScheduleDayOfMonth is capped at 28 so the schedule also fires in February.-RemoveTestDatabase is baked into the wrapper): an unattended recurring job would otherwise leave a full-size copy behind on every run until the volume fills up. -KeepTestDatabase opts out.-SqlCredential creates job/step/schedule from a workstation but is deliberately NOT embedded into the generated wrapper - the job runs under the SQL Agent service account's Windows identity; a password in a script on disk would be the wrong trade.RestoreTest_ prefix rule is validated at job-creation time, so an invalid -TestDatabaseName fails immediately instead of at the first scheduled run months later.-Confirm:$false -EnableException -NoOpen, so a failure makes the Agent job go red (a restore test that silently does nothing is worse than none: no evidence while everyone assumes the obligation is covered), and no browser is launched in a session-less context.begin block with "Ausnahme beim Aufrufen von SourceExists ... Protokolle, auf die kein Zugriff moeglich war: Security" - a System.Security.SecurityException.[System.Diagnostics.EventLog]::SourceExists() scans ALL event logs - including the Security log, which requires elevated rights - when the source does not yet exist. Under a low-privilege account (e.g. the SQL Agent service account running the function's own Agent job) that throws. In both Invoke-sqmRestoreDatabase (since 1.9.14.0) and Repair-sqmAlwaysOnDatabases the SourceExists() call sat OUTSIDE the try/catch (only the New-EventLog inside was guarded), so the exception aborted the entire operation. It only surfaced on machines where the sqmAlwaysOn event source did not already exist (on servers where a prior elevated run created it, SourceExists() returns immediately and never scans Security) - which is why it wasn't seen before on established production servers.SourceExists()/New-EventLog block in try/catch. Event-log integration is best-effort; if the source can't be checked or created, a WARNING is logged and the restore/repair continues. The later Write-EventLog calls were already -ErrorAction SilentlyContinue.SourceExists() (Compare-sqmAlwaysOnLogins, Compare-sqmAlwaysOnRoles, Sync-sqmLoginsToAlwaysOn, Invoke-sqmTempSysadminAction) already had it inside try/catch and were not affected.Invoke-DbaQuery @connParams, but $connParams was never defined anywhere in the function - Get-sqmSpnReport is otherwise purely WMI/CIM/registry- and setspn-based and had no SQL-connection concept. The splat of a non-existent variable meant Invoke-DbaQuery was called without its mandatory -SqlInstance, always threw a parameter-binding error, and the surrounding try/catch swallowed it as a WARNING - so the entire "check AG listener SPNs" feature (documented in the function's help) had silently never worked.-SqlCredential parameter and now build $connParams properly for the listener query: the SQL target is the host name for a default instance or Host\Instance for a named instance, with -SqlCredential forwarded when supplied. The block is guarded by a check for Invoke-DbaQuery (dbatools) and skips cleanly (with a VERBOSE note) when dbatools isn't present, since the core setspn-based report doesn't need it.Invoke-sqmRestoreDatabase with the given parameters baked into a generated wrapper script (same wrapper/CmdExec pattern as New-sqmAlwaysOnRepairJob / New-sqmAutoLoginSyncJob). Lets a restore run on the SQL server itself as the Agent service account instead of interactively from a remote workstation.Start-DbaAgentJob) or via the function's -StartJob switch.Invoke-sqmRestoreDatabase (-BackupFile / -BackupFiles, -DatabaseName, -NewDatabaseName, file-path overrides, -BackupBeforeRestore, -NoUserExport, -KeepAlwaysOn, -AvailabilityGroupName, -WithNoRecovery, -ContinueWithNoRecovery, -ForceSingleUser, -NoRejoinAvailabilityGroup) plus job-management parameters (-JobName, -StepName, -Force, -StartJob). The generated step uses -Confirm:$false -EnableException so a restore failure makes the Agent job fail visibly.FunctionsToExport and the GUI category map (Backup & Recovery).Export-DbaUser failed with a generic SMO "exception occurred while trying to enumerate the collection" / "exception occurred while executing a Transact-SQL statement" error that did not happen before that change.AvailabilityGroup.PrimaryReplicaServerName can report the AG replica under a different string form than the -SqlInstance value the caller passed in (FQDN vs short name, different casing) even when it is the exact same machine - a pure formatting difference that can still break Kerberos delegation for the permission-enumeration queries Export-DbaUser needs. Since 1.9.19.0 started using that resolved name ($workInstance) instead of the caller's original string for every operation, this surfaced for the first time.-SqlInstance by short hostname (case-insensitive, domain suffix stripped) rather than exact string equality; when they refer to the same machine, the caller's original -SqlInstance string is kept for $workInstance instead of substituting the AG-reported name, avoiding the format mismatch entirely.AvailabilityGroup.PrimaryReplicaServerName) - restoring or altering a database against a Secondary was never meaningful. Previously, Restore-DbaDatabase and every step downstream of it (user import, orphan-user repair, stale-login removal, owner assignment, the MULTI_USER revert) ran against whatever instance the function happened to be called with, which only worked by coincidence when that happened to be the Primary.-SqlInstance - unchanged.-SqlInstance (that view is available cluster-wide from any replica), but everything else - the database-exists/ already-single-user check, the optional pre-restore backup, user export, single-user mode, the restore, and all post-restore cleanup - now consistently uses the resolved working instance.-BackupBeforeRestore no longer excludes AG-managed databases. It now behaves identically whether or not the database is AG-managed, always running against the working instance - previously it silently did nothing for an AG database regardless of whether the switch was passed, which was confusing.$primaryInstance null bug fixed in 1.9.17.0 was patched with a fallback rather than fixed at the source. Root cause: primary-replica detection filtered Get-DbaAgReplica's results by Role -eq 'Primary', but that per-replica Role value can transiently report something other than exactly 'Primary' (e.g. 'Resolving'), so the filter can come back empty. This had always been a latent bug in the original code - it just never surfaced when the function happened to be run directly against the actual primary replica, which was the case for every prior successful use of this function. It only got hit once a run went through a path where that wasn't guaranteed.Get-DbaAgReplica | Where-Object Role -eq 'Primary'/'Secondary' pattern (used both to determine the primary in the AG-removal step and to find secondaries for the seeding-mode check in the rejoin step) with AvailabilityGroup.PrimaryReplicaServerName - a dedicated SMO property on the AG object itself, and the authoritative source for "which replica is primary" rather than something inferred from possibly-transient per-replica state. Secondaries are now simply "every replica whose name isn't the primary", removing the dependency on Role matching an exact string entirely.Get-DbaAgReplica didn't return a replica with Role -eq 'Primary' (e.g. AG mid-transition, or SMO reporting a non-'Primary' role like 'Resolving'/'Unknown' at query time), $primaryReplica was $null, and $primaryReplica.Name -ne $SqlInstance evaluated true (comparing $null to a real string), so $primaryInstance was set to $null instead of falling back to the connected instance. Every later AG operation using $primaryInstance - including the rejoin step - then failed with "Cannot bind parameter 'SqlInstance' because it is null".$primaryInstance can no longer end up $null: if the primary replica can't be positively identified, it now falls back to the connected $SqlInstance with a clear WARNING logged, instead of crashing on a null-parameter bind.-EnableException, if any of the non-critical post-restore cleanup steps (6-9: user re-import, orphan-user repair, stale Windows-login removal, owner assignment) threw, the exception propagated straight past the AG rejoin step (10) to the outer catch, so a database that was successfully restored could still end up left outside the AG with no rejoin attempt at all.finally block, gated by a new $restoreSucceeded flag (set only once the restore itself has actually completed). finally in PowerShell always runs even when an exception is rethrown from catch, so the rejoin is now guaranteed to be attempted whenever the database was AG-managed and the restore succeeded - regardless of what happens in the cleanup steps afterward. (If the restore itself failed or never ran, $restoreSucceeded stays false and rejoin is correctly skipped, since attempting to add a possibly broken/missing database back into the AG would be wrong.)$finalDbName was only ever assigned at the start of the restore step, so if the function failed earlier (e.g. AG removal) after $wasSingleUser was already set (possible since 1.9.14.0's already-SINGLE_USER-at-start check runs even earlier), the finally block's MULTI_USER revert referenced $null and would have produced a broken ALTER DATABASE [] SET MULTI_USER;. $finalDbName now defaults to $DatabaseName from the start of the run.-AvailabilityGroupName override from earlier in 1.9.14.0). That's backwards for an environment where every database on an AG-capable instance must be on AlwaysOn, including a database being restored/deployed there for the very first time.-AvailabilityGroupName wasn't given: if the instance has exactly one Availability Group, the restored database is automatically added to it (with seeding); with zero AGs there's nothing to join and it correctly stays standalone; with more than one AG the run aborts and requires -AvailabilityGroupName to disambiguate, rather than guessing.-KeepAlwaysOn now doubles as the deliberate opt-out for this auto-join, for a restore that must genuinely stay standalone (e.g. a scratch/test copy) even though the instance has an AG.setspn -S commands plus a trailing setspn -L verification command) that can be selected and copied as-is, in addition to the existing annotated command list.SpnReport_SetSpnCommands_<Timestamp>.txt) and copied directly to the Windows clipboard (Set-Clipboard) - ready to paste straight into an email or ticket for the AD team, with setspn -L check commands for every affected (deduped) account appended at the end. Clipboard failures (e.g. non-interactive session) are logged as a WARNING without blocking the run; the file is still written either way.Get-DbaAgDatabase); once a database has actually been removed from the AG, it no longer shows up as an AG member, so a retry would silently skip secondary cleanup and the rejoin/reseed step entirely - with no error, since as far as the function could tell, it was never an AG database to begin with.-AvailabilityGroupName to force AG-aware handling regardless of current live membership, for exactly this retry scenario (or restoring a brand-new database straight into an existing AG). The AG-removal step now tolerates the database already not being a member (skips Remove-DbaAgDatabase gracefully) while still cleaning up secondaries and performing the rejoin/reseed at the end.Repair-sqmAlwaysOnDatabases already creates) on both success and failure, so a failed reseed is visible to monitoring/alerting even if nobody inspects the returned result objects - this is exactly the kind of failure that must never go unnoticed.Export-DbaUser would still fail immediately with the same "database is already open and can only have one user at a time" error, since it can't get a connection either. The database is now checked via $targetDb.UserAccess immediately after connecting and, if not Multiple, reset to MULTI_USER right away (disconnecting whatever was holding it) before anything else runs.Add-DbaAgDatabase's automatic seeding of the secondaries) was gated behind an opt-in -RejoinAvailabilityGroup switch, so by default a restored AG database was left standalone and the secondaries never got the database back at all. Replaced with -NoRejoinAvailabilityGroup: rejoining (and therefore reseeding the secondaries) is now the default for any database that was removed from an AG for the restore.Export-DbaUser failing with "Database '\<db\>' is already open and can only have one user at a time" (surfaced as a WARNING, silently producing an incomplete/empty user export). The database was being switched to SINGLE_USER *before* the user export step, but Export-DbaUser opens its own SMO connection to script out users/permissions, which collides with single-user mode. Single-user is now applied after the user export instead, right before the AG-removal/ restore steps that actually need it.-KeepAlwaysOn triggers AG rejoin+reseeding after restore (it actually aborts the restore if the database is still an AG member).-CompareLogins now also compares, per login, which databases it is mapped into as a database user and which db_* roles it holds there (via Get-DbaDbUser + Get-DbaDbRoleMember) - the standard post-migration login check beyond just the server-level SID/default-DB/roles comparison already added in 1.9.11.0. A database where the mapping is missing entirely on one side is Critical (broken access); a role-set difference where both sides are mapped is Warning. Reported as one summary row per affected login, listing only the databases that actually differ.sys.configurations 'default language' / 'default full-text language', via SMO Configuration.DefaultLanguage / DefaultFullTextLanguage), complementing the already-existing per-login default language check.Compare-sqmServerConfiguration previously only diffed a handful of sp_configure/instance properties and (optionally) database name/owner/recovery model/collation, and returned raw objects with no report. Extended for post-migration verification:-CompareLogins switch: compares server logins between Source/Target (existence, SID, default database/language, disabled state, server-role membership, and password hash for SQL logins), with -IncludeSystemLogins, -Login, -ExcludeLogin filters matching the conventions of Compare-sqmAlwaysOnLogins.-IncludeMigrationObjects switch: compares Linked Servers, Credentials, SQL Agent Jobs, Endpoints, and Database Mail profiles between the two instances.ProductLevel, HostPlatform, IsClustered, IsHadrEnabled, XpCmdShell, ClrEnabled, ErrorLogPath, MasterDBPath.Status (OK/Warning/Critical), missing logins/objects and SID or password-hash mismatches are Critical, most config drift is Warning.ConvertTo-sqmHtmlReport theme) with -OutputPath, -NoOpen, -NoReport, auto-opened like the other Compare-* functions.-CompareDatabases: the system-database filter checked -not $_.IsSystemObject, but the helper's returned object never carried that property (always $null), so master/model/msdb/tempdb were never actually excluded from the comparison.msmdsrv.ini via the service command line, backs it up, and updates the <DeploymentMode> element (0 = Multidimensional, 2 = Tabular), optionally restarting the service. Refuses to proceed when existing database folders are found under the instance's Data directory (the two modes use incompatible storage formats) unless -Force is passed. Supports -WhatIf/-Confirm.is_master_key_encrypted_by_server from sys.symmetric_keys, but that column actually lives on sys.databases - the query always failed silently (caught by -ErrorAction SilentlyContinue), so HasDatabaseMasterKey and DmkEncryptedBySmk were always $false regardless of the real server state. Found while generating demo reports against DEV01 for the website. Fixed by joining sys.symmetric_keys (existence/modify_date) with sys.databases (encryption flag).Get-sqmSysadminAccounts, Get-sqmADGroupMembers, Get-sqmADGroupMembersRecursive, and Get-sqmADMemberGroups. The three AD group functions gained a new -NoOpen switch and now use Invoke-sqmOpenReport for consistency (HTML takes precedence when auto-opening).ConvertTo-sqmHtmlReport helper, status-colored ok/warn/crit cells) to 8 reporting functions that previously only wrote TXT/CSV: Get-sqmBlockingReport, Get-sqmDatabaseHealth, Get-sqmDistributedAgHealth, Get-sqmServiceBrokerHealth, Get-sqmCertificateReport, Export-sqmAlwaysOnConfiguration, Get-sqmSpnReport, Get-sqmDeadlockReport. Get-sqmServiceBrokerHealth and Get-sqmCertificateReport gained an -NoOpen switch and now use the shared Invoke-sqmOpenReport helper instead of notepad.exe/no auto-open, for consistency with the rest of the module (HTML takes precedence over TXT when opening).Find-sqmADUser: searches Active Directory for user accounts by a SamAccountName wildcard pattern (e.g. so_* for service accounts). RSAT (Get-ADUser -Filter) path with automatic LDAP/ADSI fallback when the ActiveDirectory module is unavailable, matching the existing dual-path pattern used by Get-sqmADAccountStatus and Get-sqmADGroupMembers.Get-sqmDiskBlockSize, New-sqmRandomSaPassword: comment-based help (SYNOPSIS, DESCRIPTION, PARAMETER, EXAMPLE, NOTES) translated from de-DE to en-US. No logic changes.Colleagues who can't remember function names can now type a sentence into the search box instead of a wildcard, e.g. "restore a database" or "disk is full".
*/? is scored against name, Synopsis, Description and parameter names of every function (a name match is weighted much higher than a match in the body text) and shown as a ranked "Best matches" list instead of grouped by category.Public/nlp-synonyms.ps1: a small, extensible keyword table for colloquial wording that doesn't appear verbatim in the help text (e.g. conjugations like "restoren").Problem reported: jobs were missing on secondary replicas; sqm_BackupExclude changes (IsActive/Reason, set via Show-sqmBackupExcludeForm) never reached the secondaries.
New-sqmOlaUsrDbBackupJob: the AlwaysOn job propagation at the end of the function only checked JobStatus -eq 'Created'. On every subsequent run (job already exists on the primary -> JobStatus = 'Updated' or 'AlreadyExists') the condition became $false and propagation to the secondaries stopped entirely - secondaries permanently lacked the jobs (or any later changes, e.g. changed schedules) after the very first run. It now propagates on 'Created', 'Updated' AND 'AlreadyExists' (New-sqmOlaUsrDbBackupJob is idempotent on the secondary via -Update anyway).Sync-sqmBackupExcludeTable: the AlwaysOn propagation only recursively called Sync-sqmBackupExcludeTable on each secondary - that only detects NEW/deleted databases locally there, but never transfers the IsActive/Reason values set by the admin on the primary. The DDL trigger (Register-sqmBackupExcludeTrigger) only covers CREATE_DATABASE/DROP_DATABASE, also without value propagation. Result: an exclusion decision set via the GUI never reached the secondaries, not even via the periodic sync job. Fix: after the structural reconciliation, the primary's current data is now additionally transferred to every secondary via MERGE (IsActive/Reason). Verified on DEV02 (MERGE logic tested against a mock table: changes correctly applied, new rows inserted, rows not present in the primary list left untouched, quote escaping in Reason correct). Real AlwaysOn propagation itself could not be verified end-to-end on DEV02 (no AG present there).Get-sqmSaLogin, now exported (previously private)
Invoke-sqmLogging (see 1.9.1.0): sqmPartitionTool's job-creation functions (New-sqmPartitionExtendJob, New-sqmPartitionRetentionJob) reuse the same SA login lookup as the existing New-sqmOla*Job functions instead of duplicating it.Invoke-sqmLogging, now exported (previously private)
sqmPartitionTool (a standalone module, RequiredModules = @('dbatools','sqmSQLTool')) is meant to reuse the established logging instead of duplicating it. Private functions aren't visible to other modules even with a declared module dependency - Invoke-sqmLogging had to be added to FunctionsToExport for that.Compare-sqmAlwaysOnRoles, server role comparison within an AlwaysOn AG
master - server principals (logins) *and* their server role membership (sysadmin, dbcreator, securityadmin, and from SQL Server 2022 also custom server roles) are not kept in sync automatically. After a failover, a login on the new primary might no longer be sysadmin (or conversely have too many rights) without this having been noticed before.Compare-sqmAlwaysOnLogins (there: login existence/attributes; here: role membership). Same pattern: AG/replica resolution, system login filter, -Login/-ExcludeLogin, -OnlyDifferences, TXT/HTML report, -FailOnDrift (Windows Event Log, source sqmSQLTool, EventId 9010 - the first free number, 9001-9009 were already taken).sysadmin membership differs (the highest-privilege role); Warning for any other differing role; OK when the role set is identical.is_fixed_role (SQL Server 2022) and an actually present custom server role; the "no AG found" path was cleanly tested (DEV02 has no AlwaysOn group, so a real multi-replica comparison could not be verified end-to-end).jobs/Sync-Job.ps1, login loss in unattended agent runs due to -Force
Sync-Job.ps1 called Sync-sqmLoginsToAlwaysOn -Force. For logins that already exist, -Force causes DROP + CREATE (not ALTER, see 1.8.18.0). If CREATE then fails (policy, something transient, AD latency, etc.), the login is gone entirely instead of merely not updated - in the unattended agent context Uwe confirmed the actual loss of several logins; manual runs without -Force went unnoticed.-Force:$false - only logins missing on the secondaries are added, existing ones are left untouched (DROP is no longer possible). -BackupLogins removed (it was only active together with -Force anyway, see if ($BackupLogins -and $Force) in Sync-sqmLoginsToAlwaysOn.ps1).Sync-sqmLoginsToAlwaysOn -Force -BackupLogins remains available (the function's default for -Force is unchanged at $true; only the agent job was switched).Copy-sqmLogins, narrowed the policy-disable window to just the actual copy call
Sync-sqmLoginsToAlwaysOn failed in certain customer environments with "Policy 'New Login_Enforce Passwort Policy' has been violated". Copy-sqmLogins -Force (default $true) passes -Force through to dbatools' Copy-DbaLogin, which does DROP + CREATE instead of ALTER for logins that already exist - every sync run therefore triggers a real CREATE_LOGIN event, which the PBM policy checks._DisablePolicy/_EnablePolicy helper functions and wrapped them tightly around the Copy-DbaLogin call (disable immediately before, enable in a dedicated finally immediately after). Connect/auth-mode check/AD check now run BEFORE the disabled window, orphan repair AFTER re-enabling. Verified on DEV02: order is now AuthModeCheck -> PolicyDisable -> CopyLogin -> RepairOrphanUsers (previously: PolicyDisable first of all).-DisablePolicy $false or when no DefaultPolicy is configured (still "Skipped", no disabling).Docs/_gen-reference.ps1, mojibake bug when run under Windows PowerShell 5.1
Get-Content $file -Raw (without -Encoding UTF8) read sqmSQLTool-reference.html (no BOM) under PS 5.1 using the system ANSI code page instead of UTF-8. Multi-byte characters (e.g. "─", emoji) were thereby decoded incorrectly and permanently corrupted as broken UTF-8 (mojibake) when written back.-Encoding UTF8. Tested under PS 5.1 and PS 7 - reference.html stays unchanged in content on regeneration (143 functions, cards/nav/overview in sync), no more mojibake.sqmSQLTool-reference.html itself was already up to date (today's docstring changes only touched .DESCRIPTION/.PARAMETER, which this generator doesn't read - only .SYNOPSIS and .EXAMPLE flow into the reference).Docs/sqmSQLTool_Anwender-Kurzanleitung.docx, new end-user guide
Show-sqmBackupExcludeForm (not administrators): opening the program, the UI/columns explained, including/excluding a database, "all active/inactive", the new length warning from 1.8.14.0, orphaned entries, important notes.Docs/sqmSQLTool_Admin-Kurzanleitung.docx, added a simple version for Part 1
New-sqmOlaUsrDbBackupJob -SqlInstance "SQL01" -Full -Log -UseExcludeTable handles step 1 (sync) and step 4 (DDL trigger) automatically. Step 2 (group permission) and step 3 (ongoing GUI maintenance) remain separate. The detail heading was updated accordingly to "In detail: the four individual steps (manual, optional)".Show-sqmBackupExcludeForm, warning display for the length of the exclusion list
-DatabaseName exclusion list.DatabaseBackup outputs @Databases as part of a RAISERROR('%s',...) line; the %s parameter is truncated at 2047 characters, which can make subsequent real error messages disappear from the job history (see the 1.8.11.0 incident on BLBNBGFATDBA3).New-sqmOlaUsrDbBackupJob, IsActive polarity in the exclude query was inverted
e.IsActive = 1 to build exclusions. The actual meaning of IsActive in sqm_BackupExclude: IsActive=1 means "this database should be backed up" (default for newly discovered databases, see Sync-sqmBackupExcludeTable), IsActive=0 means "don't back up". So WHERE IsActive = 1 excluded exactly the databases that should be backed up and backed up the ones explicitly deactivated (IsActive=0) - polarity completely inverted.e.IsActive = 0.IsActive=1 (AlwaysOnTest, amazon, DeadlockCollector, dtcSN, OperationsManagerDW, pdRessourcen, ReportServerTempDB, Solutioninfo, SolutioninfoSTA) and correctly skipped the 3 with IsActive=0 (SSISDB, TestDB, ReportServer).Sync-sqmBackupExcludeTable (default IsActive=1 for new databases) was correct from the start and did NOT need to be changed - the bug was solely in the read direction of this one query.New-sqmOlaUsrDbBackupJob, an explicit -BackupDirectory was being overwritten
\Usr-db was appended to EVERY resolved backup path, even when the caller had explicitly passed -BackupDirectory as a complete target path. Fix: \Usr-db is now only appended to automatically resolved paths (registry / sqlSrv.BackupDirectory / default); an explicitly set -BackupDirectory is used unchanged as the target path.-BackupDirectory "C:\Temp\ExplicitTestPath" results in exactly this path without a suffix.New-sqmOlaUsrDbBackupJob, exclude prefix ! was never valid Ola syntax
DatabaseBackup job failed): the ! prefix for exclusions introduced in v1.8.6.0 (USER_DATABASES,!db1,!db2) is never evaluated anywhere by Ola Hallengren's MaintenanceSolution.sql - it only recognizes -db1 as an exclude marker (DatabaseItem LIKE '-%'). All ! entries were therefore interpreted as positive (non-existent) database names: exclusions never took effect, and the resulting long "do not exist" warning list blew past the 2047-character limit of RAISERROR('%s', ...), which made the actual error message invisible in the job history.-. Additionally added a filter EXISTS (SELECT 1 FROM sys.databases ...), so only databases that actually exist on the respective instance are added to the exclude list - prevents the "do not exist" list from growing again in a sqm_BackupExclude table maintained across instances.Show-sqmBackupExcludeForm, job details in the info panel correct again
Load-JobInfo used to parse @Databases, @Directory etc. from the step command. After the switch to the procedure architecture (v1.8.7) the step only contains EXEC master.dbo.[sqm_Run_...] - the parameters live in the procedure body.OBJECT_DEFINITION() and parse it from there. Falls back to the step command for older jobs without a procedure.@Databases in ExcludeTable mode is now read from the DECLARE statement (instead of from @Databases = @Databases in the EXECUTE call).New-sqmOlaUsrDbBackupJob, fixed three runtime errors
CREATE/DROP PROCEDURE master.dbo.[name] was invalid SQL (database prefix not allowed in DDL statements). Fix: prefix removed; -Database master on the Invoke-DbaQuery connection sets the context correctly.Set-DbaAgentJobStep -StepId doesn't exist in the dbatools parameter set. Fix: uses -StepName (backup jobs: "DatabaseBackup $StepSuffix", sync job: 'Sync sqm_BackupExclude').New-sqmOlaUsrDbBackupJob, new defaults + auto-setup with -UseExcludeTable
New-sqmOlaUsrDbBackupJob, helper procedure in master instead of inline T-SQL in the job step
EXEC master.dbo.[sqm_Run_{JobName}]. The actual backup code is created as a stored procedure in master. The proc name is derived from the job name (special characters → underscore). Result in the Agent window: the job step is readable at a glance. The procedure is freshly DROP+CREATE'd on every call (including -Update). On AlwaysOn propagation, the secondaries also get their own proc.New-sqmOlaUsrDbBackupJob, fixed UseExcludeTable job step SQL
@ExcludeDatabases as an Ola parameter - this doesn't exist. Ola's DatabaseBackup only knows @Databases with !-prefix syntax for exclusions (USER_DATABASES,!db1,!db2). Removed dynamic SQL via sp_executesql; the step now directly builds @Databases = @Databases + ',' + @Exclusions.FOR XML PATH ... .value() in the agent job step failed with a QUOTED_IDENTIFIER error when the session option wasn't set. Added SET QUOTED_IDENTIFIER ON; at the start of the step SQL.New-sqmOlaUsrDbBackupJob
-CreateSyncJob ([bool], default $true). When -UseExcludeTable is active, a SQL Agent job is automatically created (sqm BackupExclude - SYNC by default, or using a customer-specific job-name prefix where one is configured). The job runs every 30 minutes via a pwsh CmdExec step and calls Sync-sqmBackupExcludeTable -SqlInstance '.'. Ensures IsActive changes from Show-sqmBackupExcludeForm are propagated to all AG secondaries without manual intervention. The job is updated on -Update; with -CreateSyncJob $false it is not created. Job name is derived from the configured job-name prefix (a customer-specific prefix is reused where set, otherwise the standard prefix applies). AG propagation: secondaries also get the sync job (recursive call with CreateSyncJob = $CreateSyncJob set).New-sqmOlaUsrDbBackupJob, fixed a configuration bug (v1.8.4.0)
Set-sqmConfig previously wrote the entire $globalConfig to config.json, which meant that on machines using a customer-specific job-name prefix, the OlaHH job names from an earlier default-prefix session could overwrite the customer-specific names. Fix (A): Set-sqmConfig now only saves explicitly passed keys (merge). Fix (B): in sqmSQLTool.psm1, config.json is loaded before the customer-specific configuration block - the customer-specific override always wins.Sync-sqmLoginsToAlwaysOn
sys.dm_hadr_availability_replica_states.role_desc returns NULL/RESOLVING when queried from a secondary - switched to sys.dm_hadr_availability_group_states.primary_replica, which contains the current primary on every replica.Write-EventLog error (AccessDenied) leaked to the console because the setter throws a non-terminating exception that catch { } doesn't catch. Added -ErrorAction SilentlyContinue (EventId 9002 / 9003).Invoke-sqmCollationChange
PropertyAssignmentException on ProcessStartInfo.CreateNoNewWindow = $true. The property can't be set when UseShellExecute hasn't been evaluated yet. Redundant anyway: with UseShellExecute = $false + RedirectStandard* = $true no console window appears in the first place.Get-sqmSQLInstanceCheck
Info) - reports server.Collation on every run.-Detailed) - lists all user databases whose collation differs from the instance collation as Warning.Compare-sqmServerConfiguration
Category = "Collation"), not only on a mismatch - important for migration checks.Extension of Grant-sqmTemporarySysadmin / Invoke-sqmTempSysadminAction:
CREATE LOGIN [DOMAIN\Account] FROM WINDOWS (instead of aborting as before).Grant-sqmTemporarySysadmin rejects non-Windows logins; the existence check is restricted to type IN ('U','G') (no SQL/certificate logins).DefaultPolicy is configured, this policy is disabled via Set-sqmSqlPolicyState -State Disable before creation and re-enabled afterwards (controllable via -DisablePolicy, default $true).-RemoveLogin in Invoke-sqmTempSysadminAction: on revocation, DROP LOGIN, but as a safety net only if the login isn't attached to any other fixed server role (other than public). Logins that already existed before are always kept.-PrimaryOnly; individual replicas can be skipped via -SkipSecondaryServers.Grant-sqmTemporarySysadmin now returns one result object per replica (including LoginExisted); new event IDs 9003 (login created), 9004 (drop skipped), 9005 (login removed).For patching/installation situations: temporarily make a login sysadmin, then automatically revoke it via a self-deleting SQL Agent job.
Grant-sqmTemporarySysadmin, grants sysadmin for -Days days. Without -StartDate immediately (inline) + a revoke job scheduled for today+X; with -StartDate a grant job on the start date and a revoke job on start date+X. Optional -TicketNumber (work order number) for the log. ConfirmImpact='High' + -WhatIf.Invoke-sqmTempSysadminAction, runs ALTER SERVER ROLE [sysadmin] ADD|DROP MEMBER, logs to the module log file + Windows Event Log (source sqmSQLTool, including the work order number) and deletes the calling job on success (sp_delete_job). On error the job is kept (as failed). Also usable for manual early revocation.sp_add_schedule @freq_type=1); job steps call the module via Import-Module sqmSQLTool (module name, no hardcoded path). The revoke job runs under the SQL Agent service account.The auto-update (AutoUpdate=$true) now detects a newer version and updates automatically from the last-used installation source - for all sources (previously only UNC updated automatically, PSGallery/GitHub only gave a hint):
Install.ps1 saves the source type + path (Set-sqmConfig -InstallSourceType/-InstallSourcePath). PSGallery installs are detected at runtime via Get-InstalledModule (new private Get-sqmInstallSource).Test-sqmModuleUpdate first checks the last source; if it's unknown/unreachable, the chain PSGallery→GitHub→UNC applies.Update-sqmModule is a dispatcher - PSGallery → Install-Module -Force (scope automatically AllUsers/CurrentUser), GitHub → download + unpack release ZIP (new Update-sqmFromGitHub), UNC/LocalDir → file copy with backup (shared Copy-sqmModuleFiles).UpdateCheckIntervalHours (default 24) via a marker file - no network calls on every import. Can still be skipped via SQMSQLTOOL_SKIP_AUTO_UPDATE=1.InstallSourceType, InstallSourcePath, UpdateCheckIntervalHours.displayName is now resolved (via Get-ADUser), instead of only showing the CN/name from Get-ADGroupMember (which for many accounts matches the login). The DisplayName column now shows the person's name. Fallback chain: displayName → CN/Name → sAMAccountName.displayName attribute was missing, InvokeGet threw an exception and the member was lost. Now reads tolerantly with the same fallback chain.Install.ps1 now ensures the mandatory dbatools dependency in the same scope before the import test runs. Previously the installer assumed dbatools was already present → on a fresh server without dbatools the import test failed. With -Scope AllUsers, dbatools is installed system-wide (no more scope mismatch where an AllUsers module can't find a dbatools that only lives in CurrentUser in other/admin sessions). If dbatools is missing, it is installed from the PSGallery (TLS 1.2 + NuGet provider are set along with it).sqmSQLTool.psm1: the call Set-DbatoolsConfig -FullName 'sql.connection.trustcert' -Value $true -Scope Session used the parameter -Scope, which Set-DbatoolsConfig (dbatools 2.8.x) doesn't have. The resulting exception was silently swallowed by the surrounding catch (only Write-Verbose), so sql.connection.trustcert was never set. Consequence: against SQL Server 2022 over TCP, virtually every dbatools connection failed with "The certificate chain was issued by an authority that is not trusted". Fix: removed -Scope Session (the setting applies session-wide anyway).$($x:N0) → .ToString('N0').COUNT(*) FILTER(...) (PostgreSQL) → SUM(CASE...); ring-buffer CPU via XML (record.value(...)) instead of JSON_VALUE; memory snapshot as a single result set (CROSS JOIN).$result[0].Column grabbed the first *column* of a DataRow instead of the row (→ 0 values). Now @($result)[0] + ISNULL(...) in SQL against DBNull.TSM configuration can now represent real environments where INCLUDE/EXCLUDE is offloaded to a separate file (INCLEXCL, e.g. ie_dsm.opt):
-UseInclExclFile: resolves the INCLEXCL option from dsm.opt and writes the managed block into the referenced include/exclude file instead of dsm.opt.-InclExclPath: specify the target file explicitly (created if needed).-ExcludePatterns: custom EXCLUDE patterns instead of the fixed three SQL types.-IncludeRule (@{ Path=...; ManagementClass=... }): a dedicated management class per path (e.g. 365 days for a 01Year directory).ValidateSet → ValidatePattern '^MC_[A-Za-z0-9._]+$'. Classes actually in use like MC_B_2.2_15.15.NA_IMG or MC_B_NL_NL_365.365.NA are no longer rejected. Backward-compatible: previously valid calls remain valid.TargetFile (the file actually written).If a dsm.opt/ie file is under the control of a vendor TSM configurator, that tool may overwrite the managed block - re-run if needed.
-SampleCount (default 6), -SampleIntervalSeconds (default 10).Checked = $true/$supportsWhatIf). This caused "Run" on exactly these commands to run unintentionally as a pure simulation instead of executing for real. The checkbox is now disabled by default (opt-in): "Run" executes for real, simulation must be checked deliberately.-SeedFromBackupHistory. As long as the snapshot history (B1) for a volume still has fewer than -MinDataPoints points, a growth rate is instead derived from msdb.dbo.backupset: per database, the data growth trend from the full-backup sizes (linear regression, from 3 points), distributed across volumes proportionally to data-file size. Bridges B1's ~5-run ramp-up time. Flagged with ForecastBasis='BackupHistory', confidence Low (report column Boot). Once B1 has enough snapshots, B1 takes over again. Only applies when the switch is set; requires read access to msdb.dbo.backupset.History\DiskHistory_<Instance>.json) and evaluated over the last -HistoryDays days via linear regression (least squares): GB/day, DaysUntilFull, and a confidence level (R²/point count: Low/Medium/High).volume_mount_point).-MinDataPoints runs (default 5), the volume is transparently reported as "forecast still collecting data (n of m)" instead of silently n/a.-HistoryPath, -MinDataPoints, -NoHistory. New output fields: DataPoints, ForecastConfidence, ForecastBasis. Report columns: GB/day, DaysFull, Conf.-WhatIf does not persist the history.mid.index_group_handle, which doesn't exist in sys.dm_db_missing_index_details (only index_handle). The query returned "Invalid column name index_group_handle" → no results. Join fixed to mid.index_handle = mig.index_handle.total_size_bytes, start_time and estimated_completion_time_ms from sys.dm_hadr_physical_seeding_stats. Corrected to the real columns database_size_bytes, start_time_utc and estimate_time_complete_utc (remaining time via DATEDIFF in ms), mapped by alias to the expected names.ars.role_start_time, which doesn't exist in sys.dm_hadr_availability_replica_states. Replaced with the valid column current_configuration_commit_start_time_utc (a UTC approximation; the Event Log, EventID 1480, remains authoritative). Added UTC comparison and NULL protection.Mechanical parameter fixes:
Get-DbaDefaultPath -Type Backup → (Get-DbaDefaultPath …).Backup (-Type doesn't exist; affected the -BackupBeforeRestore path).Restore-DbaDatabase -FileListOnly → Read-DbaBackupHeader -FileList (Restore-DbaDatabase has no -FileListOnly; the verify path already correctly used -VerifyOnly).Set-DbaAgentJob -OperatorToEmail → -EmailOperator.Connect-DbaInstance -EnableException → -ErrorAction Stop (Connect-DbaInstance has no -EnableException).Redesigns (cmdlet doesn't exist at all):
Update-DbaDbStatistic - this cmdlet doesn't exist, so the function had no effect. Reimplemented via Invoke-DbaQuery with a real UPDATE STATISTICS; target statistics are determined server-side from sys.stats/sys.dm_db_stats_properties, so -OnlyModified, -Index, -Table, -Statistics and -SamplePercent (FULLSCAN/SAMPLE) now take effect.Set-DbaService -StartMode doesn't exist. dbatools' Get-DbaService returns CIM instances of the SqlService class; the StartMode is now set via their CIM method SetStartMode(UInt32) (Automatic=2, Manual=3, Disabled=4). Works under PS 5.1 and 7.Get-DbaAgentServiceAccount doesn't exist. The agent service account now comes from sys.dm_server_services (locale-robust LIKE '%Agent%') via the existing SQL connection.Export-DbaUser -Force → -Force doesn't exist; now uses -FilePath (full path) without -Force. (Fixes "A parameter cannot be found that matches parameter name 'Force'".)Restore-DbaDatabase -NewDatabaseName/-DatabaseFilePath/-LogFilePath → these parameters don't exist. The (possibly new) target name now goes through -DatabaseName ($finalDbName); the physical file names/paths are handled by the already-built -FileMapping. Renaming + moving is thereby version-stable.$DatabaseName_ was interpreted as an (empty) variable, so the DB name was missing from the name; now ${DatabaseName} and a correct timestamp format.return $results in the process block are now plain return; the end block returns the list exactly once.Get-DbaAvailabilityGroup -Database (this parameter doesn't exist; the parameter-binding error is terminating and isn't caught by -ErrorAction SilentlyContinue). AG membership is now checked via Get-DbaAgDatabase, and the AG object is reloaded via the AG name.-WhatIf leaked via $WhatIfPreference into the internal Out-File/New-Item calls and produced "What if: Output to File" noise while no log was written at all. Both calls now run with -WhatIf:$false (logging is a side channel and must not be subject to ShouldProcess).-WhatIf/-EnableException. Closes the call in SQLSetupTool\Modules\PostInstall.psm1 that previously had no target.Version bump past the (misnamed) tag v1.5.0, so the accumulated fixes 1.4.8 - 1.4.15 are unambiguously the newest version on GitHub and get picked up by the update mechanism. Content identical to 1.4.15.0 (see entries below); no new function code.
-Overwrite failed with "A parameter cannot be found that matches parameter name 'Force'". Remove-DbaAgentJob -Force doesn't exist in every dbatools version; now uses -Confirm:$false (version-stable, as in all other job functions).ActiveStartTimeOfDay, version-dependent New-DbaAgentSchedule parameters, duplicate schedules). The schedule is now created via native msdb procedures (sp_add_schedule / sp_attach_schedule); duplicates removed beforehand by schedule_id.Import-Module plus Compare-sqmAlwaysOnLogins -FailOnDrift. No hashtable, no paths, no server name.-FailOnDrift. On login drift (Warning/Critical), Windows Event 9001 (Splunk) is written and an exception is thrown, so the SQL Agent job turns red (drift alarm via the OnFailure operator). Implies -NoOpen; the report is written beforehand.en-US (module config Language + Get-sqmString fallback). Affects all strings localized via Get-sqmString. Note: reports that are still hardcoded German remain German until the multi-language migration is complete.Force, BackupLogins (each $true) and BackupRetentionDays (7) are defaults. A bare Sync-sqmLoginsToAlwaysOn keeps the secondaries fully in sync (SqlInstance = computer name, AG = the first one found, paths from the settings). Opt out via -Force:$false / -BackupLogins:$false.Import-Module plus the parameterless call Sync-sqmLoginsToAlwaysOn. No hashtable, no paths, no server name, no AG in the step.schedule_id in a loop before creating a new one, instead of via an ambiguous @schedule_name.New-DbaAgentSchedule ("A parameter cannot be found that matches parameter name 'Force'", "… 'sch_…' is not a valid value for the Schedule variable"). This cmdlet's parameters vary by dbatools version. The schedule is now created via native msdb procedures (sp_add_schedule / sp_attach_schedule) through Invoke-DbaQuery - equally stable across every SQL Server and dbatools version, no more API guessing.ActiveStartTimeOfDay is an SMO property, not a New-DbaAgentSchedule parameter. The schedule now correctly uses -StartTime (format HHMMSS), -FrequencyRecurrenceFactor for Weekly/Monthly, and -FrequencySubdayType Hours / -FrequencySubdayInterval for hourly.sa wasn't recognized. Caused by an ordering bug - the dynamic sysadmin detection ran before $srcConnParams was defined, failed silently, and fell back to the literal 'sa'. This allowed an sa renamed on one node to end up in the copy batch (SID collision 0x01 on the target).sa is now additionally identified via the well-known SID 0x01 (name-independent) and is fundamentally never copied - not even with -IncludeSystemLogins.OR sid = 0x01.most_recent_sql_handle correctly read from sys.dm_exec_connections instead of sys.dm_exec_sessions (error "Invalid column name 'most_recent_sql_handle'").SqlInstance was bound twice when creating the schedule (error "parameter 'SqlInstance' is specified more than once") - now only explicitly once.Sync-sqmLoginsToAlwaysOn; on error throw → SQL Agent marks the job as failed (operator notification). No more hardcoded paths in the step.-BackupRetentionDays) and the AD orphan audit (-AuditAdOrphans, detection-only, Event Log 9003). -BackupPath uses the configured output path (Get-sqmDefaultOutputPath) instead of a fixed literal - all paths come from the settings.Manual, safe removal of Windows logins whose AD account no longer exists ("dead" AD logins). Deliberately manual only, not for unattended operation.
-AdModuleAction Abort); no deletion without ADConfirmImpact = High: -WhatIf / -Confirm apply-Force and -BackupLogins active by default: the running job keeps the secondaries fully in sync (password/language/default-DB drift), with a rollback backup. Opt out via -Force:$false / -BackupLogins:$false-BackupRetentionDays (default 7): cleans up backups, sync logs and audit reports-AuditAdOrphans: reports orphaned Windows logins after every run (sync log + Event Log EventId 9003 for Splunk) - detection only, no auto-deletepassword_hash read from sys.sql_logins instead of sys.server_principals (error "Invalid column name 'password_hash'" with -BackupLogins)name instead of the non-existent column creation_dateRemoteSigned, always stage locally first (removing Mark-of-the-Web), so execution from a UNC/\\tsclient\ path is not blockedComprehensive HTML hardware report for local and remote systems:
FunctionsToExport in sqmSQLTool.psd1 switched from the wildcard pattern *-sqm* to an explicit list of all 103 functionsImport-Module sqmSQLToolInteractive presentations at www.powershelldba.de/Praesentation/:
(Earlier versions not documented)