Private/smgGui.ps1
|
<#
WinForms-Aufbau und Logik der GUI. Alle Handler greifen ueber $script:smgUi auf die Controls zu (Modul-Scope), nicht ueber Variablen der aufrufenden Funktion - so laufen sie auch nach Show-sqmMergeGeneratorGui -PassThru noch korrekt. #> $script:smgIgnore = '<nicht uebernehmen>' $script:smgNone = '<keine>' $script:smgChangeTypes = [ordered]@{ 'Typ 2 - Historie' = 'Type2' 'Typ 1 - Ueberschreiben' = 'Type1' 'Fest - nie aendern' = 'Fixed' } # =========================================================================================== # Konfiguration # =========================================================================================== function Read-smgConfig { $cfg = [pscustomobject]@{ Servers = @(); Auth = 'Windows'; Login = '' } try { if (Test-Path $script:smgConfigPath) { $j = Get-Content $script:smgConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json $cfg.Servers = @($j.Servers | Where-Object { $_ }) if ($j.Auth) { $cfg.Auth = $j.Auth } if ($j.Login) { $cfg.Login = $j.Login } } } catch { } $cfg } function Save-smgConfig { param([string]$Server, [string]$Auth, [string]$Login) try { $cfg = Read-smgConfig $servers = @($Server) + @($cfg.Servers | Where-Object { $_ -and $_ -ne $Server }) | Select-Object -First 10 $dir = Split-Path $script:smgConfigPath if (-not (Test-Path $dir)) { [void](New-Item -ItemType Directory -Path $dir -Force) } [pscustomobject]@{ Servers = @($servers); Auth = $Auth; Login = $Login } | ConvertTo-Json | Set-Content -Path $script:smgConfigPath -Encoding UTF8 } catch { } } # =========================================================================================== # Formularaufbau # =========================================================================================== function New-smgLabel([string]$Text) { $l = New-Object System.Windows.Forms.Label $l.Text = $Text $l.AutoSize = $true $l.Anchor = 'Left' $l.Margin = New-Object System.Windows.Forms.Padding(3, 7, 3, 3) $l } function New-smgFieldLabel([string]$Text) { # Fuer TableLayoutPanel-Zellen: vertikal zentriert statt fester Top-Margin $l = New-smgLabel $Text $l.Margin = New-Object System.Windows.Forms.Padding(3, 0, 3, 0) $l } function New-smgCombo([int]$Width, [switch]$Editable) { $c = New-Object System.Windows.Forms.ComboBox $c.DropDownStyle = if ($Editable) { 'DropDown' } else { 'DropDownList' } if ($Width) { $c.Width = $Width } else { $c.Anchor = 'Left, Right' } $c.Margin = New-Object System.Windows.Forms.Padding(3, 4, 3, 3) $c } function New-smgButton([string]$Text, [int]$Width = 110) { $b = New-Object System.Windows.Forms.Button $b.Text = $Text $b.Width = $Width $b.Height = 27 $b } function New-smgMainForm { $ui = @{ Loading = $false; Instance = $null; Cred = $null SrcTables = @(); TgtTables = @(); SrcCols = @(); TgtCols = @() KeyNames = @(); KeyAutoPick = $true; KeyOverride = $null } $script:smgUi = $ui $form = New-Object System.Windows.Forms.Form $form.Text = "sqmMergeGenerator $script:smgModuleVersion - T-SQL MERGE Generator" $form.Size = New-Object System.Drawing.Size(1420, 940) $form.MinimumSize = New-Object System.Drawing.Size(1100, 760) $form.StartPosition = 'CenterScreen' $form.Font = New-Object System.Drawing.Font('Segoe UI', 9) $ui.Form = $form $root = New-Object System.Windows.Forms.TableLayoutPanel $root.Dock = 'Fill' $root.ColumnCount = 1 $root.RowCount = 3 $root.Padding = New-Object System.Windows.Forms.Padding(6) [void]$root.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 62))) [void]$root.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 62))) [void]$root.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 100))) # --------------------------------------------------------------------------------------- # Verbindung # --------------------------------------------------------------------------------------- $gConn = New-Object System.Windows.Forms.GroupBox $gConn.Text = 'Verbindung' $gConn.Dock = 'Fill' $fConn = New-Object System.Windows.Forms.FlowLayoutPanel $fConn.Dock = 'Fill' $fConn.WrapContents = $false $ui.Server = New-smgCombo 240 -Editable $ui.Auth = New-smgCombo 150 [void]$ui.Auth.Items.AddRange(@('Windows-Authentifizierung', 'SQL-Authentifizierung')) $ui.Login = New-Object System.Windows.Forms.TextBox; $ui.Login.Width = 130 $ui.Password = New-Object System.Windows.Forms.TextBox; $ui.Password.Width = 130; $ui.Password.UseSystemPasswordChar = $true foreach ($t in $ui.Login, $ui.Password) { $t.Margin = New-Object System.Windows.Forms.Padding(3, 4, 3, 3) } $ui.Trust = New-Object System.Windows.Forms.CheckBox $ui.Trust.Text = 'TrustServerCertificate'; $ui.Trust.Checked = $true; $ui.Trust.AutoSize = $true $ui.Trust.Margin = New-Object System.Windows.Forms.Padding(8, 6, 3, 3) $ui.Connect = New-smgButton 'Verbinden' $ui.Status = New-smgLabel '' $ui.Status.ForeColor = [System.Drawing.Color]::DimGray $fConn.Controls.AddRange(@( (New-smgLabel 'Server:'), $ui.Server, (New-smgLabel 'Anmeldung:'), $ui.Auth, (New-smgLabel 'Login:'), $ui.Login, (New-smgLabel 'Passwort:'), $ui.Password, $ui.Trust, $ui.Connect, $ui.Status)) $gConn.Controls.Add($fConn) $root.Controls.Add($gConn, 0, 0) # --------------------------------------------------------------------------------------- # Quelle / Ziel # --------------------------------------------------------------------------------------- $tSel = New-Object System.Windows.Forms.TableLayoutPanel $tSel.Dock = 'Fill'; $tSel.ColumnCount = 2; $tSel.RowCount = 1; $tSel.Margin = New-Object System.Windows.Forms.Padding(0) [void]$tSel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 100))) [void]$tSel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 50))) [void]$tSel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 50))) foreach ($side in 'Src', 'Tgt') { $g = New-Object System.Windows.Forms.GroupBox $g.Text = if ($side -eq 'Src') { 'Quelle' } else { 'Ziel' } $g.Dock = 'Fill' $t = New-Object System.Windows.Forms.TableLayoutPanel $t.Dock = 'Fill'; $t.ColumnCount = 4; $t.RowCount = 1 [void]$t.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 100))) [void]$t.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('AutoSize'))) [void]$t.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 38))) [void]$t.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('AutoSize'))) [void]$t.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 62))) $ui["${side}Db"] = New-smgCombo 0 $ui["${side}Table"] = New-smgCombo 0 $ui["${side}Table"].MaxDropDownItems = 25 $t.Controls.Add((New-smgFieldLabel 'Datenbank:'), 0, 0) $t.Controls.Add($ui["${side}Db"], 1, 0) $t.Controls.Add((New-smgFieldLabel 'Tabelle:'), 2, 0) $t.Controls.Add($ui["${side}Table"], 3, 0) $g.Controls.Add($t) $tSel.Controls.Add($g, $(if ($side -eq 'Src') { 0 } else { 1 }), 0) } $root.Controls.Add($tSel, 0, 1) # --------------------------------------------------------------------------------------- # Split: oben Spalten + Optionen, unten Statement # --------------------------------------------------------------------------------------- $split = New-Object System.Windows.Forms.SplitContainer $split.Dock = 'Fill' $split.Orientation = 'Horizontal' $ui.Split = $split $root.Controls.Add($split, 0, 2) $tTop = New-Object System.Windows.Forms.TableLayoutPanel $tTop.Dock = 'Fill'; $tTop.ColumnCount = 1; $tTop.RowCount = 2; $tTop.Margin = New-Object System.Windows.Forms.Padding(0) [void]$tTop.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 100))) [void]$tTop.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 92))) $split.Panel1.Controls.Add($tTop) $tCols = New-Object System.Windows.Forms.TableLayoutPanel $tCols.Dock = 'Fill'; $tCols.ColumnCount = 3; $tCols.RowCount = 1; $tCols.Margin = New-Object System.Windows.Forms.Padding(0) [void]$tCols.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 24))) [void]$tCols.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 54))) [void]$tCols.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 22))) $tTop.Controls.Add($tCols, 0, 0) # 1. Quellspalten $g1 = New-Object System.Windows.Forms.GroupBox; $g1.Text = '1. Quellspalten'; $g1.Dock = 'Fill' $t1 = New-Object System.Windows.Forms.TableLayoutPanel; $t1.Dock = 'Fill'; $t1.RowCount = 2; $t1.ColumnCount = 1 [void]$t1.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 100))) [void]$t1.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 34))) $ui.SrcList = New-Object System.Windows.Forms.CheckedListBox $ui.SrcList.Dock = 'Fill'; $ui.SrcList.CheckOnClick = $true; $ui.SrcList.IntegralHeight = $false $f1 = New-Object System.Windows.Forms.FlowLayoutPanel; $f1.Dock = 'Fill' $ui.SrcAll = New-smgButton 'Alle' 70 $ui.SrcNone = New-smgButton 'Keine' 70 $f1.Controls.AddRange(@($ui.SrcAll, $ui.SrcNone)) $t1.Controls.Add($ui.SrcList, 0, 0); $t1.Controls.Add($f1, 0, 1) $g1.Controls.Add($t1) $tCols.Controls.Add($g1, 0, 0) # 2. Mapping $g2 = New-Object System.Windows.Forms.GroupBox; $g2.Text = '2. Mapping Quelle -> Ziel'; $g2.Dock = 'Fill' $grid = New-Object System.Windows.Forms.DataGridView $grid.Dock = 'Fill' $grid.AllowUserToAddRows = $false $grid.AllowUserToDeleteRows = $false $grid.AllowUserToResizeRows = $false $grid.RowHeadersVisible = $false $grid.SelectionMode = 'CellSelect' $grid.EditMode = 'EditOnEnter' $grid.BackgroundColor = [System.Drawing.SystemColors]::Window $grid.AutoSizeColumnsMode = 'Fill' $grid.ShowCellToolTips = $true $c = New-Object System.Windows.Forms.DataGridViewTextBoxColumn $c.Name = 'Src'; $c.HeaderText = 'Quellspalte'; $c.ReadOnly = $true; $c.FillWeight = 26; [void]$grid.Columns.Add($c) $c = New-Object System.Windows.Forms.DataGridViewTextBoxColumn $c.Name = 'SrcType'; $c.HeaderText = 'Typ'; $c.ReadOnly = $true; $c.FillWeight = 16 $c.DefaultCellStyle.ForeColor = [System.Drawing.Color]::DimGray; [void]$grid.Columns.Add($c) $c = New-Object System.Windows.Forms.DataGridViewComboBoxColumn $c.Name = 'Tgt'; $c.HeaderText = 'Zielspalte'; $c.FillWeight = 28; $c.FlatStyle = 'Flat'; [void]$grid.Columns.Add($c) $c = New-Object System.Windows.Forms.DataGridViewTextBoxColumn $c.Name = 'TgtType'; $c.HeaderText = 'Typ'; $c.ReadOnly = $true; $c.FillWeight = 16 $c.DefaultCellStyle.ForeColor = [System.Drawing.Color]::DimGray; [void]$grid.Columns.Add($c) $c = New-Object System.Windows.Forms.DataGridViewComboBoxColumn $c.Name = 'Change'; $c.HeaderText = 'Aenderungsart (SCD)'; $c.FillWeight = 22; $c.FlatStyle = 'Flat' $c.ToolTipText = 'Typ 2: neue Version (nur SCD-2-Modus) | Typ 1: ueberschreiben | Fest: nie aktualisieren' [void]$c.Items.AddRange([string[]]@($script:smgChangeTypes.Keys)) [void]$grid.Columns.Add($c) $ui.Grid = $grid $g2.Controls.Add($grid) $tCols.Controls.Add($g2, 1, 0) # 3. Schluessel $g3 = New-Object System.Windows.Forms.GroupBox; $g3.Text = '3. Schluesselspalten (Join)'; $g3.Dock = 'Fill' $ui.KeyList = New-Object System.Windows.Forms.CheckedListBox $ui.KeyList.Dock = 'Fill'; $ui.KeyList.CheckOnClick = $true; $ui.KeyList.IntegralHeight = $false $g3.Controls.Add($ui.KeyList) $tCols.Controls.Add($g3, 2, 0) # Optionen $gOpt = New-Object System.Windows.Forms.GroupBox; $gOpt.Text = 'Optionen'; $gOpt.Dock = 'Fill' $tOpt = New-Object System.Windows.Forms.TableLayoutPanel; $tOpt.Dock = 'Fill'; $tOpt.RowCount = 2; $tOpt.ColumnCount = 1 [void]$tOpt.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 32))) [void]$tOpt.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 34))) $fo1 = New-Object System.Windows.Forms.FlowLayoutPanel; $fo1.Dock = 'Fill'; $fo1.WrapContents = $false $fo2 = New-Object System.Windows.Forms.FlowLayoutPanel; $fo2.Dock = 'Fill'; $fo2.WrapContents = $false $mk = { param($type, $text, $checked) $o = New-Object "System.Windows.Forms.$type" $o.Text = $text; $o.AutoSize = $true; $o.Checked = $checked $o.Margin = New-Object System.Windows.Forms.Padding(3, 5, 12, 3) $o } $ui.ModeUpsert = & $mk RadioButton 'Upsert / SCD Typ 1' $true $ui.ModeScd2 = & $mk RadioButton 'SCD Typ 2 (Historie)' $false $ui.OnlyChanged = & $mk CheckBox 'Nur bei Aenderung aktualisieren' $true $ui.NotInSource = & $mk CheckBox 'Nicht in Quelle: loeschen' $false $ui.HoldLock = & $mk CheckBox 'WITH (HOLDLOCK)' $true $ui.OutputAct = & $mk CheckBox 'OUTPUT $action' $false $ui.DupCheck = & $mk CheckBox 'Duplikatpruefung Quelle' $true $ui.ModeUpsert.Font = New-Object System.Drawing.Font($form.Font, [System.Drawing.FontStyle]::Bold) $ui.ModeScd2.Font = $ui.ModeUpsert.Font $fo1.Controls.AddRange(@($ui.ModeUpsert, $ui.ModeScd2, $ui.OnlyChanged, $ui.NotInSource, $ui.HoldLock, $ui.OutputAct, $ui.DupCheck)) $ui.ValidFrom = New-smgCombo 140 $ui.ValidTo = New-smgCombo 140 $ui.CurrentFlag = New-smgCombo 140 $ui.OpenEnd = New-smgCombo 150 -Editable [void]$ui.OpenEnd.Items.AddRange(@('NULL', "'9999-12-31'", "'9999-12-31 23:59:59'")) $ui.OpenEnd.Text = 'NULL' $ui.Filter = New-Object System.Windows.Forms.TextBox; $ui.Filter.Width = 320 $ui.Filter.Margin = New-Object System.Windows.Forms.Padding(3, 4, 3, 3) $ui.ScdLabels = @((New-smgLabel 'Gueltig ab:'), (New-smgLabel 'Gueltig bis:'), (New-smgLabel 'Aktuell-Flag:'), (New-smgLabel 'Offenes Ende:')) $fo2.Controls.AddRange(@( $ui.ScdLabels[0], $ui.ValidFrom, $ui.ScdLabels[1], $ui.ValidTo, $ui.ScdLabels[2], $ui.CurrentFlag, $ui.ScdLabels[3], $ui.OpenEnd, (New-smgLabel ' Quellfilter WHERE:'), $ui.Filter)) $tOpt.Controls.Add($fo1, 0, 0); $tOpt.Controls.Add($fo2, 0, 1) $gOpt.Controls.Add($tOpt) $tTop.Controls.Add($gOpt, 0, 1) # Statement $tBot = New-Object System.Windows.Forms.TableLayoutPanel $tBot.Dock = 'Fill'; $tBot.ColumnCount = 1; $tBot.RowCount = 2 [void]$tBot.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 100))) [void]$tBot.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 38))) $ui.Output = New-Object System.Windows.Forms.TextBox $ui.Output.Multiline = $true; $ui.Output.ScrollBars = 'Both'; $ui.Output.WordWrap = $false $ui.Output.Dock = 'Fill'; $ui.Output.ReadOnly = $true $ui.Output.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 252) $ui.Output.Font = New-Object System.Drawing.Font('Consolas', 10) $fb = New-Object System.Windows.Forms.FlowLayoutPanel; $fb.Dock = 'Fill' $ui.BtnCopy = New-smgButton 'In Zwischenablage' 130 $ui.BtnSave = New-smgButton 'Speichern ...' 110 $ui.BtnCheck = New-smgButton 'Syntax pruefen' 120 $ui.BtnExecute = New-smgButton 'Ausfuehren ...' 110 $fb.Controls.AddRange(@($ui.BtnCopy, $ui.BtnSave, $ui.BtnCheck, $ui.BtnExecute)) $tBot.Controls.Add($ui.Output, 0, 0); $tBot.Controls.Add($fb, 0, 1) $split.Panel2.Controls.Add($tBot) $form.Controls.Add($root) Register-smgEvents Set-smgModeState $form.Add_Shown({ try { $script:smgUi.Split.SplitterDistance = [int]($script:smgUi.Split.Height * 0.52) } catch { } }) $form } # =========================================================================================== # Events # =========================================================================================== function Register-smgEvents { $ui = $script:smgUi $ui.Auth.Add_SelectedIndexChanged({ $sql = $script:smgUi.Auth.SelectedIndex -eq 1 $script:smgUi.Login.Enabled = $sql $script:smgUi.Password.Enabled = $sql }) $ui.Connect.Add_Click({ Connect-smgServer }) $ui.Password.Add_KeyDown({ if ($_.KeyCode -eq 'Return') { $_.SuppressKeyPress = $true; Connect-smgServer } }) $ui.Server.Add_KeyDown({ if ($_.KeyCode -eq 'Return') { $_.SuppressKeyPress = $true; Connect-smgServer } }) $ui.SrcDb.Add_SelectedIndexChanged({ Update-smgTableList -Side Src }) $ui.TgtDb.Add_SelectedIndexChanged({ Update-smgTableList -Side Tgt }) $ui.SrcTable.Add_SelectedIndexChanged({ Update-smgColumns -Side Src }) $ui.TgtTable.Add_SelectedIndexChanged({ Update-smgColumns -Side Tgt }) $ui.SrcList.Add_ItemCheck({ if ($script:smgUi.Loading) { return } Update-smgMapping -OverrideIndex $_.Index -OverrideChecked ($_.NewValue -eq 'Checked') }) $ui.SrcAll.Add_Click({ Set-smgAllSourceChecked $true }) $ui.SrcNone.Add_Click({ Set-smgAllSourceChecked $false }) $ui.Grid.Add_CurrentCellDirtyStateChanged({ if ($script:smgUi.Grid.IsCurrentCellDirty) { [void]$script:smgUi.Grid.CommitEdit('CurrentCellChange') } }) $ui.Grid.Add_CellValueChanged({ if ($script:smgUi.Loading -or $_.RowIndex -lt 0) { return } if ($script:smgUi.Grid.Columns[$_.ColumnIndex].Name -eq 'Tgt') { $row = $script:smgUi.Grid.Rows[$_.RowIndex] $tc = $script:smgUi.TgtCols | Where-Object { $_.ColumnName -eq $row.Cells['Tgt'].Value } | Select-Object -First 1 $row.Cells['TgtType'].Value = if ($tc) { $tc.TypeDisplay } else { '' } Update-smgRowStyle $row Update-smgKeyList } Update-smgStatement }) $ui.Grid.Add_DataError({ $_.ThrowException = $false }) $ui.KeyList.Add_ItemCheck({ if ($script:smgUi.Loading) { return } $script:smgUi.KeyOverride = @{ Index = $_.Index; Checked = ($_.NewValue -eq 'Checked') } Update-smgStatement $script:smgUi.KeyOverride = $null }) $ui.ModeUpsert.Add_CheckedChanged({ Set-smgModeState; Update-smgStatement }) foreach ($cb in $ui.OnlyChanged, $ui.NotInSource, $ui.HoldLock, $ui.OutputAct, $ui.DupCheck) { $cb.Add_CheckedChanged({ Update-smgStatement }) } foreach ($cb in $ui.ValidFrom, $ui.ValidTo, $ui.CurrentFlag, $ui.OpenEnd) { $cb.Add_SelectedIndexChanged({ Update-smgStatement }) } $ui.OpenEnd.Add_TextChanged({ Update-smgStatement }) $ui.Filter.Add_TextChanged({ Update-smgStatement }) $ui.BtnCopy.Add_Click({ if ($script:smgUi.Output.Text) { [System.Windows.Forms.Clipboard]::SetText($script:smgUi.Output.Text) Set-smgStatus 'Statement in die Zwischenablage kopiert.' } }) $ui.BtnSave.Add_Click({ Save-smgStatement }) $ui.BtnCheck.Add_Click({ Invoke-smgStatement -CheckOnly }) $ui.BtnExecute.Add_Click({ Invoke-smgStatement }) } # =========================================================================================== # Logik # =========================================================================================== function Set-smgStatus([string]$Text, [switch]$IsError) { $ui = $script:smgUi $ui.Status.Text = $Text $ui.Status.ForeColor = if ($IsError) { [System.Drawing.Color]::Firebrick } else { [System.Drawing.Color]::DarkGreen } } function Get-smgCurrentScd2 { [bool]$script:smgUi.ModeScd2.Checked } function Set-smgModeState { $ui = $script:smgUi $scd = Get-smgCurrentScd2 foreach ($c in @($ui.ValidFrom, $ui.ValidTo, $ui.CurrentFlag, $ui.OpenEnd) + $ui.ScdLabels) { $c.Enabled = $scd } $ui.OnlyChanged.Enabled = -not $scd $ui.NotInSource.Text = if ($scd) { 'Nicht in Quelle: Version schliessen' } else { 'Nicht in Quelle: loeschen' } $ui.OutputAct.Enabled = -not $scd } function Connect-smgServer { $ui = $script:smgUi $server = $ui.Server.Text.Trim() if (-not $server) { Set-smgStatus 'Bitte einen Server angeben.' -IsError; return } $cred = $null if ($ui.Auth.SelectedIndex -eq 1) { if (-not $ui.Login.Text) { Set-smgStatus 'Bitte Login angeben.' -IsError; return } $pw = New-Object System.Security.SecureString foreach ($ch in $ui.Password.Text.ToCharArray()) { $pw.AppendChar($ch) } $cred = New-Object System.Management.Automation.PSCredential($ui.Login.Text, $pw) } $ui.Form.Cursor = 'WaitCursor' try { $cs = New-smgConnectionString -SqlInstance $server -SqlCredential $cred -TrustServerCertificate $ui.Trust.Checked $ver = Invoke-smgSqlQuery -ConnectionString $cs -Query "SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(30)) AS V, CAST(SERVERPROPERTY('Edition') AS nvarchar(100)) AS E" $dbs = @(Invoke-smgSqlQuery -ConnectionString $cs -Query "SELECT name FROM sys.databases WHERE state = 0 AND HAS_DBACCESS(name) = 1 ORDER BY CASE WHEN database_id <= 4 THEN 1 ELSE 0 END, name" | ForEach-Object name) $ui.Instance = $server; $ui.Cred = $cred; $ui.ConnBase = $cs $ui.Loading = $true foreach ($cb in $ui.SrcDb, $ui.TgtDb, $ui.SrcTable, $ui.TgtTable) { $cb.Items.Clear() } $ui.SrcList.Items.Clear(); $ui.KeyList.Items.Clear(); $ui.Grid.Rows.Clear() $ui.SrcCols = @(); $ui.TgtCols = @() foreach ($d in $dbs) { [void]$ui.SrcDb.Items.Add($d); [void]$ui.TgtDb.Items.Add($d) } $ui.Loading = $false Set-smgStatus ("Verbunden: {0} ({1}, {2}) - {3} Datenbanken" -f $server, $ver.V, $ver.E, $dbs.Count) $authName = if ($cred) { 'SQL' } else { 'Windows' } Save-smgConfig -Server $server -Auth $authName -Login $ui.Login.Text if (-not $ui.Server.Items.Contains($server)) { $ui.Server.Items.Insert(0, $server) } Update-smgStatement } catch { $ui.Loading = $false Set-smgStatus ("Verbindung fehlgeschlagen: {0}" -f (Get-smgErrorText $_)) -IsError } finally { $ui.Form.Cursor = 'Default' } } function Get-smgErrorText($err) { $e = $err.Exception while ($e.InnerException) { $e = $e.InnerException } $e.Message } function Update-smgTableList { param([ValidateSet('Src', 'Tgt')][string]$Side) $ui = $script:smgUi if ($ui.Loading -or -not $ui.ConnBase) { return } $db = $ui["${Side}Db"].SelectedItem if (-not $db) { return } try { $cs = Set-smgConnectionDatabase -ConnectionString $ui.ConnBase -Database $db $views = if ($Side -eq 'Src') { 1 } else { 0 } $tables = @(Invoke-smgSqlQuery -ConnectionString $cs -Parameters @{ V = $views } -Query @" SELECT s.name AS SchemaName, o.name AS TableName, o.type AS T FROM sys.objects AS o JOIN sys.schemas AS s ON s.schema_id = o.schema_id WHERE o.is_ms_shipped = 0 AND (o.type = 'U' OR (@V = 1 AND o.type = 'V')) ORDER BY s.name, o.name; "@) $ui.Loading = $true $combo = $ui["${Side}Table"] $combo.Items.Clear() foreach ($t in $tables) { $suffix = if ($t.T.Trim() -eq 'V') { ' (View)' } else { '' } [void]$combo.Items.Add(('{0}.{1}{2}' -f $t.SchemaName, $t.TableName, $suffix)) } $ui["${Side}Tables"] = $tables $ui.Loading = $false if ($Side -eq 'Src') { $ui.SrcCols = @(); $ui.SrcList.Items.Clear() } else { $ui.TgtCols = @() } Update-smgMapping -Reset Set-smgStatus ("[{0}]: {1} Objekte" -f $db, $tables.Count) } catch { $ui.Loading = $false Set-smgStatus ("Tabellen konnten nicht gelesen werden: {0}" -f (Get-smgErrorText $_)) -IsError } } function Get-smgSelectedTable([string]$Side) { $ui = $script:smgUi $i = $ui["${Side}Table"].SelectedIndex if ($i -lt 0 -or -not $ui["${Side}Db"].SelectedItem) { return $null } $t = @($ui["${Side}Tables"])[$i] [pscustomobject]@{ Database = [string]$ui["${Side}Db"].SelectedItem; Schema = $t.SchemaName; Table = $t.TableName } } function Update-smgColumns { param([ValidateSet('Src', 'Tgt')][string]$Side) $ui = $script:smgUi if ($ui.Loading) { return } $sel = Get-smgSelectedTable $Side if (-not $sel) { return } try { $cols = @(Get-sqmMergeColumn -SqlInstance $ui.Instance -SqlCredential $ui.Cred -Database $sel.Database -Schema $sel.Schema -Table $sel.Table) } catch { Set-smgStatus ("Spalten konnten nicht gelesen werden: {0}" -f (Get-smgErrorText $_)) -IsError return } $ui.Loading = $true try { if ($Side -eq 'Src') { $ui.SrcCols = $cols $ui.SrcList.Items.Clear() foreach ($c in $cols) { $flag = if ($c.IsPrimaryKey) { ' [PK]' } else { '' } # rowversion/berechnete Spalten der Quelle sind lesbar, aber selten sinnvoll [void]$ui.SrcList.Items.Add(('{0} {1}{2}' -f $c.ColumnName, $c.TypeDisplay, $flag), (-not $c.IsRowVersion)) } } else { $ui.TgtCols = $cols Update-smgScdCombos } $ui.KeyAutoPick = $true } finally { $ui.Loading = $false } Update-smgMapping -Reset Set-smgStatus ("{0}.{1}.{2}: {3} Spalten" -f $sel.Database, $sel.Schema, $sel.Table, $cols.Count) } function Update-smgScdCombos { $ui = $script:smgUi $names = @($ui.TgtCols | Where-Object IsWritable | ForEach-Object ColumnName) $pick = @{ ValidFrom = '^(Valid_?From|G(ue|ü)ltig_?(Ab|Von)|Start_?Date|Effective_?(From|Date)|Date_?From|Von_?Datum|Row_?Start.*)$' ValidTo = '^(Valid_?To|G(ue|ü)ltig_?Bis|End_?Date|Expir(y|ation)_?Date|Effective_?To|Date_?To|Bis_?Datum|Row_?End.*)$' CurrentFlag = '^(Is_?Current|Current_?Flag|Is_?Active|Ist_?Aktuell|Aktuell|Current)$' } foreach ($k in $pick.Keys) { $cb = $ui[$k] $cb.Items.Clear() [void]$cb.Items.Add($script:smgNone) foreach ($n in $names) { [void]$cb.Items.Add($n) } $hit = $names | Where-Object { $_ -match $pick[$k] } | Select-Object -First 1 $cb.SelectedItem = if ($hit) { $hit } else { $script:smgNone } } # Eine Historisierungs-Struktur in der Zieltabelle schaltet direkt in den SCD-2-Modus if (($ui.ValidTo.SelectedItem -ne $script:smgNone) -or ($ui.CurrentFlag.SelectedItem -ne $script:smgNone)) { $ui.ModeScd2.Checked = $true } else { $ui.ModeUpsert.Checked = $true } Set-smgModeState } function Get-smgScdColumnNames { $ui = $script:smgUi @($ui.ValidFrom, $ui.ValidTo, $ui.CurrentFlag) | ForEach-Object { $_.SelectedItem } | Where-Object { $_ -and $_ -ne $script:smgNone } } function Set-smgAllSourceChecked([bool]$Checked) { $ui = $script:smgUi $ui.Loading = $true for ($i = 0; $i -lt $ui.SrcList.Items.Count; $i++) { $ui.SrcList.SetItemChecked($i, $Checked) } $ui.Loading = $false Update-smgMapping } function Update-smgRowStyle($row) { $ignored = $row.Cells['Tgt'].Value -eq $script:smgIgnore $row.DefaultCellStyle.ForeColor = if ($ignored) { [System.Drawing.Color]::Gray } else { [System.Drawing.SystemColors]::ControlText } } function Update-smgMapping { param([int]$OverrideIndex = -1, [bool]$OverrideChecked, [switch]$Reset) $ui = $script:smgUi $grid = $ui.Grid $prev = @{} if (-not $Reset) { foreach ($r in $grid.Rows) { $prev[[string]$r.Cells['Src'].Value] = @{ Tgt = $r.Cells['Tgt'].Value; Change = $r.Cells['Change'].Value } } } $ui.Loading = $true try { $grid.Rows.Clear() $tgtColumn = $grid.Columns['Tgt'] $tgtColumn.Items.Clear() [void]$tgtColumn.Items.Add($script:smgIgnore) $writable = @($ui.TgtCols | Where-Object IsWritable) foreach ($c in $writable) { [void]$tgtColumn.Items.Add($c.ColumnName) } $scdNames = @(Get-smgScdColumnNames) $scd = Get-smgCurrentScd2 $defaultChange = if ($scd) { 'Typ 2 - Historie' } else { 'Typ 1 - Ueberschreiben' } for ($i = 0; $i -lt $ui.SrcCols.Count; $i++) { $checked = if ($i -eq $OverrideIndex) { $OverrideChecked } else { $ui.SrcList.GetItemChecked($i) } if (-not $checked) { continue } $sc = $ui.SrcCols[$i] if ($prev.ContainsKey($sc.ColumnName)) { $tgt = $prev[$sc.ColumnName].Tgt $chg = $prev[$sc.ColumnName].Change } else { $m = $writable | Where-Object { $_.ColumnName -eq $sc.ColumnName } | Select-Object -First 1 $tgt = $script:smgIgnore if ($m -and $m.ColumnName -notin $scdNames -and -not ($scd -and $m.IsIdentity)) { $tgt = $m.ColumnName } $chg = $defaultChange } if (-not $tgt -or -not $tgtColumn.Items.Contains($tgt)) { $tgt = $script:smgIgnore } if (-not $chg) { $chg = $defaultChange } $tc = $writable | Where-Object { $_.ColumnName -eq $tgt } | Select-Object -First 1 $idx = $grid.Rows.Add() $row = $grid.Rows[$idx] $row.Cells['Src'].Value = $sc.ColumnName $row.Cells['SrcType'].Value = $sc.TypeDisplay $row.Cells['Tgt'].Value = $tgt $row.Cells['TgtType'].Value = if ($tc) { $tc.TypeDisplay } else { '' } $row.Cells['Change'].Value = $chg Update-smgRowStyle $row } } finally { $ui.Loading = $false } Update-smgKeyList Update-smgStatement } function Get-smgMappingFromGrid { $ui = $script:smgUi foreach ($r in $ui.Grid.Rows) { $tgt = [string]$r.Cells['Tgt'].Value if (-not $tgt -or $tgt -eq $script:smgIgnore) { continue } $tc = $ui.TgtCols | Where-Object { $_.ColumnName -eq $tgt } | Select-Object -First 1 $label = [string]$r.Cells['Change'].Value [pscustomobject]@{ SourceColumn = [string]$r.Cells['Src'].Value TargetColumn = $tgt ChangeType = if ($script:smgChangeTypes.Contains($label)) { $script:smgChangeTypes[$label] } else { $null } TargetDataType = if ($tc) { $tc.BaseType } else { $null } TargetIsClrType = if ($tc) { $tc.IsClrType } else { $false } TargetIsIdentity = if ($tc) { $tc.IsIdentity } else { $false } } } } function Update-smgKeyList { $ui = $script:smgUi $prevChecked = @{} for ($i = 0; $i -lt $ui.KeyList.Items.Count; $i++) { if ($ui.KeyList.GetItemChecked($i)) { $prevChecked[$ui.KeyNames[$i]] = $true } } $map = @(Get-smgMappingFromGrid) $autoPick = $ui.KeyAutoPick -and $map.Count -gt 0 # Vorbelegung: PK des Ziels. Ist der nicht gemappt (typisch SCD 2: PK = Surrogatschluessel), # dann der PK der Quelle als fachlicher Schluessel. $pkSide = 'Tgt' if ($autoPick) { $tgtPkMapped = $map | Where-Object { $tn = $_.TargetColumn; $ui.TgtCols | Where-Object { $_.ColumnName -eq $tn -and $_.IsPrimaryKey } } if (-not $tgtPkMapped) { $pkSide = 'Src' } } $wasLoading = $ui.Loading $ui.Loading = $true try { $ui.KeyList.Items.Clear() $names = @() foreach ($m in $map) { $tc = $ui.TgtCols | Where-Object { $_.ColumnName -eq $m.TargetColumn } | Select-Object -First 1 $sc = $ui.SrcCols | Where-Object { $_.ColumnName -eq $m.SourceColumn } | Select-Object -First 1 $check = if (-not $autoPick) { $prevChecked.ContainsKey($m.TargetColumn) } elseif ($pkSide -eq 'Tgt') { [bool]($tc -and $tc.IsPrimaryKey) } else { [bool]($sc -and $sc.IsPrimaryKey) } $text = if ($m.SourceColumn -eq $m.TargetColumn) { $m.TargetColumn } else { '{0} <- {1}' -f $m.TargetColumn, $m.SourceColumn } if ($tc -and $tc.IsPrimaryKey) { $text += ' [PK]' } [void]$ui.KeyList.Items.Add($text, $check) $names += $m.TargetColumn } $ui.KeyNames = $names if ($autoPick) { $ui.KeyAutoPick = $false } } finally { $ui.Loading = $wasLoading } } function Get-smgKeyColumns { $ui = $script:smgUi for ($i = 0; $i -lt $ui.KeyList.Items.Count; $i++) { $checked = $ui.KeyList.GetItemChecked($i) if ($ui.KeyOverride -and $ui.KeyOverride.Index -eq $i) { $checked = $ui.KeyOverride.Checked } if ($checked) { $ui.KeyNames[$i] } } } function Get-smgStatementParameters { $ui = $script:smgUi $src = Get-smgSelectedTable 'Src' $tgt = Get-smgSelectedTable 'Tgt' if (-not $ui.ConnBase) { return '-- Schritt 1: Server verbinden.' } if (-not $src) { return '-- Schritt 2: Quelldatenbank und -tabelle waehlen.' } if (-not $tgt) { return '-- Schritt 3: Zieldatenbank und -tabelle waehlen.' } $map = @(Get-smgMappingFromGrid) if ($map.Count -eq 0) { return '-- Schritt 4: Mindestens eine Quellspalte einer Zielspalte zuordnen.' } $keys = @(Get-smgKeyColumns) if ($keys.Count -eq 0) { return '-- Schritt 5: Schluesselspalte(n) fuer den Join anhaken.' } $p = @{ SourceDatabase = $src.Database; SourceSchema = $src.Schema; SourceTable = $src.Table TargetDatabase = $tgt.Database; TargetSchema = $tgt.Schema; TargetTable = $tgt.Table ColumnMapping = $map; KeyColumns = $keys UpdateOnlyChanged = [bool]$ui.OnlyChanged.Checked DeleteNotMatchedBySource = [bool]$ui.NotInSource.Checked HoldLock = [bool]$ui.HoldLock.Checked OutputAction = [bool]$ui.OutputAct.Checked DuplicateKeyCheck = [bool]$ui.DupCheck.Checked SourceFilter = $ui.Filter.Text } if (Get-smgCurrentScd2) { $p.Mode = 'Scd2' $p.OutputAction = $false foreach ($k in 'ValidFrom', 'ValidTo', 'CurrentFlag') { $v = $ui[$k].SelectedItem if ($v -and $v -ne $script:smgNone) { $p["${k}Column"] = [string]$v } } $p.OpenEndValue = $ui.OpenEnd.Text.Trim() } $p } function Update-smgStatement { $ui = $script:smgUi if (-not $ui -or $ui.Loading) { return } $p = Get-smgStatementParameters if ($p -is [string]) { $ui.Output.Text = $p; $ui.Statement = $null; return } try { $sql = New-sqmMergeStatement @p $ui.Output.Text = $sql $ui.Statement = $sql } catch { $ui.Output.Text = "-- Kein Statement: $(Get-smgErrorText $_)" $ui.Statement = $null } } function Save-smgStatement { $ui = $script:smgUi if (-not $ui.Statement) { Set-smgStatus 'Kein gueltiges Statement zum Speichern.' -IsError; return } $dlg = New-Object System.Windows.Forms.SaveFileDialog $dlg.Filter = 'SQL-Skript (*.sql)|*.sql|Alle Dateien (*.*)|*.*' $tgt = Get-smgSelectedTable 'Tgt' $dlg.FileName = 'MERGE_{0}_{1}.sql' -f $tgt.Schema, $tgt.Table if ($dlg.ShowDialog($ui.Form) -eq 'OK') { [System.IO.File]::WriteAllText($dlg.FileName, $ui.Statement, (New-Object System.Text.UTF8Encoding($true))) Set-smgStatus "Gespeichert: $($dlg.FileName)" } } function Invoke-smgStatement { param([switch]$CheckOnly, [switch]$Force) $ui = $script:smgUi if (-not $ui.Statement) { Set-smgStatus 'Kein gueltiges Statement.' -IsError; return } $tgt = Get-smgSelectedTable 'Tgt' if (-not $CheckOnly -and -not $Force) { $msg = "Das MERGE-Statement wird jetzt gegen`r`n`r`n {0} -> [{1}].[{2}].[{3}]`r`n`r`nausgefuehrt und veraendert Daten. Fortfahren?" -f $ui.Instance, $tgt.Database, $tgt.Schema, $tgt.Table $r = [System.Windows.Forms.MessageBox]::Show($ui.Form, $msg, 'MERGE ausfuehren', 'YesNo', 'Warning', 'Button2') if ($r -ne 'Yes') { return } } $sql = if ($CheckOnly) { "SET NOEXEC ON;`r`n$($ui.Statement)`r`nSET NOEXEC OFF;" } else { $ui.Statement } $ui.Form.Cursor = 'WaitCursor' try { $cs = Set-smgConnectionDatabase -ConnectionString $ui.ConnBase -Database $tgt.Database if ($CheckOnly) { [void](Invoke-smgSqlQuery -ConnectionString $cs -Query $sql -NonQuery -CommandTimeout 0) Set-smgStatus 'Syntax und Objektnamen OK (kompiliert mit SET NOEXEC ON, nichts ausgefuehrt).' } elseif ($ui.Statement -match 'AS NeueSchluessel') { # SCD 2: das Statement liefert selbst eine Zusammenfassung $r = Invoke-smgSqlQuery -ConnectionString $cs -Query $sql -CommandTimeout 0 | Select-Object -First 1 $text = 'Ausgefuehrt: {0} neue Schluessel, {1} neue Versionen, {2} geschlossen, {3} Typ-1-Updates' -f [int]$r.NeueSchluessel, [int]$r.NeueVersionen, [int]$r.GeschlosseneSchluessel, [int]$r.Typ1Aktualisiert Set-smgStatus $text $r } else { $n = Invoke-smgSqlQuery -ConnectionString $cs -Query $sql -NonQuery -CommandTimeout 0 Set-smgStatus ("Ausgefuehrt: {0} Zeile(n) betroffen." -f $n) $n } } catch { $text = Get-smgErrorText $_ Set-smgStatus ("Fehler: {0}" -f $text) -IsError if (-not $Force) { [void][System.Windows.Forms.MessageBox]::Show($ui.Form, $text, 'SQL-Fehler', 'OK', 'Error') } } finally { $ui.Form.Cursor = 'Default' } } |