Skip to content

Add WPF SQLCMD GUI prototype #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,32 @@
# sqlcmd-gui
An attempt to implement a graphical user interface based on SQLCMD for executing parameterized TSQL scripts

This repository contains a minimal Windows desktop application for executing parameterized SQL scripts with `sqlcmd`.

The application allows you to:

1. Browse for a `.sql` file.
2. Automatically detect SQLCMD variables in the script that are not defined by `:setvar` commands and prompt for their values.
3. Enter SQL Server connection information.
4. Execute the script via the `sqlcmd` command-line tool and display the output.

The source is implemented as a WPF project targeting .NET 6.0.

## Building

Use the .NET SDK on Windows:

```bash
dotnet build SqlcmdGuiApp/SqlcmdGuiApp.csproj
```

## Running

After building, run the produced executable or start it with `dotnet run`:

```bash
dotnet run --project SqlcmdGuiApp/SqlcmdGuiApp.csproj
```

You need the `sqlcmd` utility available in your PATH for the execution step to work.

Any unhandled errors encountered by the application are written to `error.log` in the application directory.
7 changes: 7 additions & 0 deletions SqlcmdGuiApp/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Application x:Class="SqlcmdGuiApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
48 changes: 48 additions & 0 deletions SqlcmdGuiApp/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace SqlcmdGuiApp
{
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
DispatcherUnhandledException += App_DispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}

internal static void LogError(string message)
{
try
{
File.AppendAllText("error.log", $"{DateTime.Now:u} {message}{Environment.NewLine}");
}
catch
{
// ignored
}
}

private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
LogError(e.Exception.ToString());
MessageBox.Show("An unexpected error occurred. See error.log for details.");
e.Handled = true;
}

private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
LogError(e.ExceptionObject.ToString());
}

private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
LogError(e.Exception.ToString());
}
}
}
62 changes: 62 additions & 0 deletions SqlcmdGuiApp/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<Window x:Class="SqlcmdGuiApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SQLCMD GUI" Height="600" Width="600">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>

<TextBox x:Name="FilePathTextBox" Grid.Column="0" Grid.Row="0" Margin="0 0 5 5"/>
<Button Content="Browse" Grid.Column="1" Grid.Row="0" Click="BrowseButton_Click"/>

<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Vertical" Margin="0 5">
<TextBlock Text="Connection Details" FontWeight="Bold"/>
<StackPanel Orientation="Horizontal" Margin="0 2">
<TextBlock Text="Server:" Width="80"/>
<TextBox x:Name="ServerTextBox" Width="200"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 2">
<TextBlock Text="Database:" Width="80"/>
<TextBox x:Name="DatabaseTextBox" Width="200"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 2">
<TextBlock Text="Authentication:" Width="80"/>
<ComboBox x:Name="AuthComboBox" Width="200" SelectionChanged="AuthComboBox_SelectionChanged">
<ComboBoxItem Content="Windows" IsSelected="True"/>
<ComboBoxItem Content="SQL"/>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 2" x:Name="SqlAuthPanel" Visibility="Collapsed">
<TextBlock Text="User:" Width="80"/>
<TextBox x:Name="UserTextBox" Width="100"/>
<TextBlock Text="Password:" Margin="10 0"/>
<PasswordBox x:Name="PasswordBox" Width="100"/>
</StackPanel>
</StackPanel>

<GroupBox Header="Parameters" Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="ParametersPanel">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding Name}" Width="150"/>
<TextBox Text="{Binding Value}" Width="200"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</GroupBox>

<Button Content="Execute" Grid.Row="3" Grid.ColumnSpan="2" Height="30" Click="ExecuteButton_Click"/>
</Grid>
</Window>
121 changes: 121 additions & 0 deletions SqlcmdGuiApp/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;

namespace SqlcmdGuiApp
{
public partial class MainWindow : Window
{
public ObservableCollection<SqlParameter> Parameters { get; } = new();

public MainWindow()
{
InitializeComponent();
ParametersPanel.ItemsSource = Parameters;
}

private void BrowseButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFileDialog { Filter = "SQL Files (*.sql)|*.sql|All Files (*.*)|*.*" };
if (dlg.ShowDialog() == true)
{
FilePathTextBox.Text = dlg.FileName;
LoadParameters(dlg.FileName);
}
}

private void LoadParameters(string path)
{
Parameters.Clear();
if (!File.Exists(path)) return;
var text = File.ReadAllText(path);
var variableRegex = new Regex(@"\$\(([^)]+)\)");
var setvarRegex = new Regex(@"^\s*:setvar\s+(\w+)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
var variables = variableRegex.Matches(text).Select(m => m.Groups[1].Value).ToHashSet();
var defined = setvarRegex.Matches(text).Select(m => m.Groups[1].Value).ToHashSet();
var needed = variables.Except(defined);
foreach (var v in needed)
{
Parameters.Add(new SqlParameter { Name = v, Value = string.Empty });
}
}

private void AuthComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
SqlAuthPanel.Visibility = AuthComboBox.SelectedIndex == 1 ? Visibility.Visible : Visibility.Collapsed;
}

private void ExecuteButton_Click(object sender, RoutedEventArgs e)
{
if (!File.Exists(FilePathTextBox.Text))
{
MessageBox.Show("SQL file not found.");
return;
}

var psi = new ProcessStartInfo("sqlcmd")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
psi.ArgumentList.Add("-S");
psi.ArgumentList.Add(ServerTextBox.Text);
if (!string.IsNullOrEmpty(DatabaseTextBox.Text))
{
psi.ArgumentList.Add("-d");
psi.ArgumentList.Add(DatabaseTextBox.Text);
}

if (AuthComboBox.SelectedIndex == 1)
{
psi.ArgumentList.Add("-U");
psi.ArgumentList.Add(UserTextBox.Text);
psi.ArgumentList.Add("-P");
psi.ArgumentList.Add(PasswordBox.Password);
}
else
{
psi.ArgumentList.Add("-E");
}

foreach (var p in Parameters)
{
psi.ArgumentList.Add("-v");
psi.ArgumentList.Add($"{p.Name}={p.Value}");
}

psi.ArgumentList.Add("-i");
psi.ArgumentList.Add(FilePathTextBox.Text);

try
{
var process = Process.Start(psi);
process.WaitForExit();
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();

var window = new OutputWindow(output, error);
window.ShowDialog();
}
catch (Exception ex)
{
App.LogError(ex.ToString());
MessageBox.Show("Failed to execute sqlcmd. See error.log for details.");
}
}
}

public class SqlParameter
{
public string Name { get; set; }
public string Value { get; set; }
}
}
8 changes: 8 additions & 0 deletions SqlcmdGuiApp/OutputWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Window x:Class="SqlcmdGuiApp.OutputWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SQLCMD Output" Height="400" Width="600">
<Grid Margin="5">
<TextBox x:Name="OutputTextBox" IsReadOnly="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"/>
</Grid>
</Window>
13 changes: 13 additions & 0 deletions SqlcmdGuiApp/OutputWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Windows;

namespace SqlcmdGuiApp
{
public partial class OutputWindow : Window
{
public OutputWindow(string output, string error)
{
InitializeComponent();
OutputTextBox.Text = string.IsNullOrWhiteSpace(error) ? output : output + "\n" + error;
}
}
}
7 changes: 7 additions & 0 deletions SqlcmdGuiApp/SqlcmdGuiApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>