WIP query tool, executes query for real and shows result
This commit is contained in:
parent
fd4cb8692d
commit
bee0e0915f
9 changed files with 204 additions and 61 deletions
|
|
@ -1,7 +1,8 @@
|
|||
# pgLabII AI Assistant Guidelines
|
||||
|
||||
## Project Context
|
||||
This is a .NET 8/C# 13 Avalonia cross-platform application for document management.
|
||||
This is a .NET 9/C# 14 Avalonia cross-platform application for querying and inspecting
|
||||
postgresql databases. It should also be a good editor for SQL files.
|
||||
|
||||
### Architecture Overview
|
||||
- **Main Project**: pgLabII (Avalonia UI)
|
||||
|
|
@ -12,16 +13,18 @@ This is a .NET 8/C# 13 Avalonia cross-platform application for document manageme
|
|||
## Coding Standards
|
||||
|
||||
### C# Guidelines
|
||||
- Use C# 13 features and modern .NET patterns
|
||||
- Use C# 14 features and modern .NET patterns
|
||||
- Prefer primary constructors for dependency injection
|
||||
- Use `var` for obvious types, explicit types for clarity
|
||||
- Implement proper async/await patterns for I/O operations
|
||||
- Use nullable reference types consistently
|
||||
- Prefer "target-typed new"
|
||||
- Prefer "list initializer" over new
|
||||
- Package versions are managed centrally in Directory.Packages.props
|
||||
|
||||
### Avalonia-Specific
|
||||
- Follow MVVM pattern strictly
|
||||
- x:Name does work for making components accessible in code-behind
|
||||
- ViewModels should inherit from appropriate base classes
|
||||
- Use ReactiveUI patterns where applicable
|
||||
- Make use of annotations to generate bindable properties
|
||||
|
|
|
|||
|
|
@ -84,9 +84,9 @@ public class EditHistoryManager : IEditHistoryManager
|
|||
|
||||
public void SaveToDatabase()
|
||||
{
|
||||
_db.EditHistory.AddRange(_pendingEdits);
|
||||
_db.SaveChanges();
|
||||
_pendingEdits.Clear();
|
||||
// _db.EditHistory.AddRange(_pendingEdits);
|
||||
// _db.SaveChanges();
|
||||
// _pendingEdits.Clear();
|
||||
}
|
||||
|
||||
public IReadOnlyList<EditHistoryEntry> GetHistory() => _pendingEdits.AsReadOnly();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive;
|
||||
using System.Threading.Tasks;
|
||||
using Npgsql;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
using pgLabII.Model;
|
||||
|
||||
namespace pgLabII.ViewModels;
|
||||
|
||||
|
|
@ -18,56 +22,50 @@ public class ColumnInfo
|
|||
|
||||
public partial class QueryToolViewModel : ViewModelBase, IViewItem
|
||||
{
|
||||
private readonly ServerConfigurationEntity? _serverConfig;
|
||||
|
||||
// Tab caption
|
||||
[Reactive] private string _caption = "Query";
|
||||
|
||||
// SQL text bound to the editor
|
||||
[Reactive] private string _userSql = "SELECT 1 AS one, 'hello' AS text;";
|
||||
[Reactive] private string _userSql = "SELECT n, 1, 'hello', '2025-4-5'::date FROM generate_series(1, 2000) AS d(n)";
|
||||
|
||||
// Summary and status labels
|
||||
[Reactive] private string _resultSummary = "Showing 0 rows";
|
||||
[Reactive] private string _status = "Ready";
|
||||
|
||||
// Paging flags
|
||||
[Reactive] private bool _canLoadMore = false;
|
||||
[Reactive] private bool _autoLoadMore = false;
|
||||
[Reactive] private bool _canLoadMore;
|
||||
[Reactive] private bool _autoLoadMore;
|
||||
|
||||
// Rows shown in the DataGrid. For now, simple object rows for AutoGenerateColumns.
|
||||
public ObservableCollection<object> Rows { get; } = new();
|
||||
public ObservableCollection<ColumnInfo> Columns { get; } = new();
|
||||
|
||||
[Reactive] private IReadOnlyList<ColumnInfo> _columns = new List<ColumnInfo>();
|
||||
|
||||
// Commands (stubs)
|
||||
// Commands
|
||||
public ReactiveCommand<Unit, Unit> RunQuery { get; }
|
||||
public ReactiveCommand<Unit, Unit> LoadMore { get; }
|
||||
public ReactiveCommand<Unit, Unit> ExportResults { get; }
|
||||
public ReactiveCommand<Unit, Unit> OpenSqlFile { get; }
|
||||
public ReactiveCommand<Unit, Unit> SaveSqlFile { get; }
|
||||
|
||||
public QueryToolViewModel()
|
||||
public QueryToolViewModel(ServerConfigurationEntity? serverConfig)
|
||||
{
|
||||
// Create stub commands that only update labels/rows so UI is interactive without real DB work.
|
||||
RunQuery = ReactiveCommand.Create(() =>
|
||||
_serverConfig = serverConfig;
|
||||
|
||||
// Create command that executes actual SQL queries
|
||||
RunQuery = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
Rows.Clear();
|
||||
Columns.Clear();
|
||||
Columns.Add(new ColumnInfo { Name = "id", DataType = typeof(int), DisplayName = "ID" });
|
||||
Columns.Add(new ColumnInfo { Name = "name", DataType = typeof(string), DisplayName = "Name" });
|
||||
Columns.Add(new ColumnInfo { Name = "active", DataType = typeof(bool), DisplayName = "Active" });
|
||||
Rows.Add(new { id = 1, name = "Alice", active = true });
|
||||
Rows.Add(new { id = 2, name = "Bob", active = false });
|
||||
this.RaisePropertyChanged(nameof(Rows)); // Force DataGrid refresh
|
||||
ResultSummary = $"Showing {Rows.Count} rows";
|
||||
Status = "Query executed (stub)";
|
||||
CanLoadMore = true;
|
||||
await ExecuteQuery();
|
||||
});
|
||||
|
||||
LoadMore = ReactiveCommand.Create(() =>
|
||||
{
|
||||
// Add more demo rows to see paging UX
|
||||
var baseId = Rows.Count;
|
||||
for (int i = 1; i <= 3; i++)
|
||||
{
|
||||
Rows.Add(new { id = baseId + i, name = $"User {baseId + i}", active = (baseId + i) % 2 == 0 });
|
||||
Rows.Add(new RowData(10));
|
||||
}
|
||||
this.RaisePropertyChanged(nameof(Rows)); // Force DataGrid refresh
|
||||
ResultSummary = $"Showing {Rows.Count} rows";
|
||||
|
|
@ -91,4 +89,117 @@ public partial class QueryToolViewModel : ViewModelBase, IViewItem
|
|||
Status = "Save SQL file (stub)";
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ExecuteQuery()
|
||||
{
|
||||
if (_serverConfig == null)
|
||||
{
|
||||
Status = "Error: No server configuration selected";
|
||||
ResultSummary = "Showing 0 rows";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(UserSql))
|
||||
{
|
||||
Status = "Error: SQL query is empty";
|
||||
ResultSummary = "Showing 0 rows";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Status = "Executing query...";
|
||||
Rows.Clear();
|
||||
|
||||
var connStringBuilder = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = _serverConfig.Host,
|
||||
Port = _serverConfig.Port,
|
||||
Database = _serverConfig.InitialDatabase,
|
||||
Username = _serverConfig.UserName,
|
||||
Password = _serverConfig.Password,
|
||||
SslMode = _serverConfig.SslMode,
|
||||
};
|
||||
|
||||
using var connection = new NpgsqlConnection(connStringBuilder.ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = new NpgsqlCommand(UserSql, connection);
|
||||
using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
// Get column information - build in a temporary list to avoid multiple CollectionChanged events
|
||||
var schema = reader.GetColumnSchema();
|
||||
var columnList = new List<ColumnInfo>();
|
||||
foreach (var column in schema)
|
||||
{
|
||||
columnList.Add(new ColumnInfo
|
||||
{
|
||||
Name = column.ColumnName ?? "Unknown",
|
||||
DisplayName = column.ColumnName,
|
||||
DataType = column.DataType ?? typeof(string),
|
||||
IsSortable = true,
|
||||
IsFilterable = true
|
||||
});
|
||||
}
|
||||
|
||||
// Read rows - also build in a temporary list first
|
||||
var rowList = new List<object>();
|
||||
int rowCount = 0;
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
var values = new object[reader.FieldCount];
|
||||
reader.GetValues(values);
|
||||
|
||||
// Convert to a dynamic object for the DataGrid
|
||||
var row = new RowData(reader.FieldCount);
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
row.Values[i] = values[i];
|
||||
}
|
||||
|
||||
rowList.Add(row);
|
||||
rowCount++;
|
||||
}
|
||||
|
||||
// Swap the entire Columns list at once (single property change notification)
|
||||
Columns = columnList;
|
||||
|
||||
// Add all rows at once
|
||||
Rows.Clear();
|
||||
foreach (var row in rowList)
|
||||
{
|
||||
Rows.Add(row);
|
||||
}
|
||||
|
||||
ResultSummary = $"Showing {rowCount} rows";
|
||||
Status = "Query executed successfully";
|
||||
CanLoadMore = false; // TODO: Implement pagination if needed
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Status = $"Error: {ex.Message}";
|
||||
ResultSummary = "Showing 0 rows";
|
||||
Rows.Clear();
|
||||
Columns = new List<ColumnInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic row container for displaying results in the DataGrid
|
||||
/// </summary>
|
||||
public class RowData
|
||||
{
|
||||
public RowData(int columnCount)
|
||||
{
|
||||
Values = new object[columnCount];
|
||||
}
|
||||
//public Dictionary<string, object?> Values { get; } = new();
|
||||
public object[] Values { get; }
|
||||
|
||||
public object? this[int idx]
|
||||
{
|
||||
get => Values[idx]; //.TryGetValue(key, out var value) ? value : null;
|
||||
set => Values[idx] = value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ public class ServerConfigurationViewModel : ReactiveObject
|
|||
var window = new Views.EditServerConfigurationWindow(new(this)) { New = false };
|
||||
window.Show();
|
||||
});
|
||||
ExploreCommand = ReactiveCommand.Create(() => {
|
||||
var window = new Views.SingleDatabaseWindow();
|
||||
ExploreCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
var window = new Views.SingleDatabaseWindow(entity);
|
||||
window.Show();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ public class ServerListViewModel : ViewModelBase
|
|||
[
|
||||
new (new()
|
||||
{
|
||||
Name = "Local pg15",
|
||||
Name = "pg18",
|
||||
ColorEnabled = true,
|
||||
Host = "localhost",
|
||||
Port = 5434,
|
||||
InitialDatabase = "dbname",
|
||||
Port = 5418,
|
||||
InitialDatabase = "postgres",
|
||||
UserName = "postgres",
|
||||
Password = "admin",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using pgLabII.Model;
|
||||
|
||||
namespace pgLabII.ViewModels;
|
||||
|
||||
|
|
@ -7,9 +8,17 @@ namespace pgLabII.ViewModels;
|
|||
/// </summary>
|
||||
public class ViewListViewModel : ViewModelBase
|
||||
{
|
||||
public ObservableCollection<IViewItem> Views { get; } = [
|
||||
new QueryToolViewModel() { Caption = "Abc" },
|
||||
new QueryToolViewModel() { Caption = "Test" } ,
|
||||
];
|
||||
private readonly ServerConfigurationEntity serverConfig;
|
||||
|
||||
public ViewListViewModel(ServerConfigurationEntity serverConfig)
|
||||
{
|
||||
this.serverConfig = serverConfig;
|
||||
|
||||
Views = [
|
||||
new QueryToolViewModel(serverConfig) { Caption = "Abc" },
|
||||
new QueryToolViewModel(serverConfig) { Caption = "Test" },
|
||||
];
|
||||
}
|
||||
public ObservableCollection<IViewItem> Views { get; private set; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,14 +7,17 @@
|
|||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600"
|
||||
x:Class="pgLabII.Views.QueryToolView"
|
||||
x:DataType="viewModels:QueryToolViewModel">
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||
<!-- Step 2: Open/Save toolbar -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="4">
|
||||
<Button Content="Open" Command="{Binding OpenSqlFile}" ToolTip.Tip="Open .sql file (Ctrl+O)" />
|
||||
<Button Content="Save" Command="{Binding SaveSqlFile}" ToolTip.Tip="Save .sql file (Ctrl+S)" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<!-- Step 2: SQL Editor -->
|
||||
<controls:CodeEditorView Grid.Row="1"
|
||||
Text="{Binding UserSql, Mode=TwoWay}" />
|
||||
<!-- Step 3: Results toolbar -->
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="4">
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="4">
|
||||
<Button Content="Run" Command="{Binding RunQuery}" />
|
||||
<Button Content="Load more" Command="{Binding LoadMore}" IsEnabled="{Binding CanLoadMore}" Margin="4,0,0,0"/>
|
||||
<Button Content="Export..." Command="{Binding ExportResults}" Margin="4,0,0,0"/>
|
||||
|
|
@ -22,18 +25,16 @@
|
|||
<TextBlock Text="{Binding ResultSummary}" Margin="16,0,0,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Status}" Margin="8,0,0,0" VerticalAlignment="Center" Foreground="Gray"/>
|
||||
</StackPanel>
|
||||
<!-- Editor and Results grid with splitter -->
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto,*">
|
||||
<controls:CodeEditorView Grid.Column="0" Text="{Binding UserSql, Mode=TwoWay}" />
|
||||
<GridSplitter Grid.Column="1" Width="6" Background="Gray" ShowsPreview="True" HorizontalAlignment="Stretch" />
|
||||
<DataGrid x:Name="ResultsGrid"
|
||||
Grid.Column="2"
|
||||
ItemsSource="{Binding Rows}"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Extended"
|
||||
CanUserSortColumns="True"
|
||||
AutoGenerateColumns="False"
|
||||
Margin="4" />
|
||||
</Grid>
|
||||
<!-- Step 4: Results grid -->
|
||||
<DataGrid Grid.Row="3"
|
||||
x:Name="ResultsDataGrid"
|
||||
ItemsSource="{Binding Rows}"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Extended"
|
||||
CanUserSortColumns="True"
|
||||
AutoGenerateColumns="False"
|
||||
Margin="4">
|
||||
<DataGrid.Columns />
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
@ -1,26 +1,41 @@
|
|||
using Avalonia;
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System.Collections.Specialized;
|
||||
using pgLabII.ViewModels;
|
||||
|
||||
namespace pgLabII.Views;
|
||||
|
||||
public partial class QueryToolView : UserControl
|
||||
{
|
||||
private QueryToolViewModel? _currentVm;
|
||||
|
||||
public QueryToolView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.DataContextChanged += (_, __) => WireColumns();
|
||||
this.DataContextChanged += (_, _) => WireColumns();
|
||||
WireColumns();
|
||||
}
|
||||
|
||||
private void WireColumns()
|
||||
{
|
||||
// Unsubscribe from previous ViewModel if it exists
|
||||
if (_currentVm != null)
|
||||
{
|
||||
_currentVm.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
if (DataContext is QueryToolViewModel vm)
|
||||
{
|
||||
var grid = this.FindControl<DataGrid>("ResultsGrid");
|
||||
vm.Columns.CollectionChanged += (s, e) => RegenerateColumns(grid, vm);
|
||||
_currentVm = vm;
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
RegenerateColumns(this.ResultsDataGrid, vm);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(QueryToolViewModel.Columns) && DataContext is QueryToolViewModel vm)
|
||||
{
|
||||
var grid = this.ResultsDataGrid;
|
||||
RegenerateColumns(grid, vm);
|
||||
}
|
||||
}
|
||||
|
|
@ -28,15 +43,17 @@ public partial class QueryToolView : UserControl
|
|||
private void RegenerateColumns(DataGrid grid, QueryToolViewModel vm)
|
||||
{
|
||||
grid.Columns.Clear();
|
||||
foreach (var col in vm.Columns)
|
||||
//foreach (var col in vm.Columns)
|
||||
for (int i = 0; i < vm.Columns.Count; i++)
|
||||
{
|
||||
var col = vm.Columns[i];
|
||||
DataGridColumn gridCol;
|
||||
if (col.DataType == typeof(bool))
|
||||
{
|
||||
gridCol = new DataGridCheckBoxColumn
|
||||
{
|
||||
Header = col.DisplayName ?? col.Name,
|
||||
Binding = new Avalonia.Data.Binding(col.Name)
|
||||
Binding = new Avalonia.Data.Binding($"Values[{i}]")
|
||||
};
|
||||
}
|
||||
else
|
||||
|
|
@ -44,7 +61,7 @@ public partial class QueryToolView : UserControl
|
|||
gridCol = new DataGridTextColumn
|
||||
{
|
||||
Header = col.DisplayName ?? col.Name,
|
||||
Binding = new Avalonia.Data.Binding(col.Name)
|
||||
Binding = new Avalonia.Data.Binding($"Values[{i}]")
|
||||
};
|
||||
}
|
||||
grid.Columns.Add(gridCol);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using pgLabII.Model;
|
||||
using pgLabII.ViewModels;
|
||||
|
||||
namespace pgLabII.Views;
|
||||
|
||||
public partial class SingleDatabaseWindow : Window
|
||||
{
|
||||
public SingleDatabaseWindow()
|
||||
public SingleDatabaseWindow(ServerConfigurationEntity serverConfig)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new ViewListViewModel();
|
||||
DataContext = new ViewListViewModel(serverConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue