From bee0e0915fbe447b326afaa064851293e93103f5 Mon Sep 17 00:00:00 2001 From: eelke Date: Sat, 25 Oct 2025 15:48:19 +0200 Subject: [PATCH] WIP query tool, executes query for real and shows result --- .ai-guidelines.md | 7 +- .../EditHistoryManager/EditHistoryManager.cs | 6 +- pgLabII/ViewModels/QueryToolViewModel.cs | 153 +++++++++++++++--- .../ServerConfigurationViewModel.cs | 5 +- pgLabII/ViewModels/ServerListViewModel.cs | 6 +- pgLabII/ViewModels/ViewListViewModel.cs | 17 +- pgLabII/Views/QueryToolView.axaml | 31 ++-- pgLabII/Views/QueryToolView.axaml.cs | 35 ++-- pgLabII/Views/SingleDatabaseWindow.axaml.cs | 5 +- 9 files changed, 204 insertions(+), 61 deletions(-) diff --git a/.ai-guidelines.md b/.ai-guidelines.md index 82b8c08..b7e1d1d 100644 --- a/.ai-guidelines.md +++ b/.ai-guidelines.md @@ -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 diff --git a/pgLabII/EditHistoryManager/EditHistoryManager.cs b/pgLabII/EditHistoryManager/EditHistoryManager.cs index 45ba53d..439bfed 100644 --- a/pgLabII/EditHistoryManager/EditHistoryManager.cs +++ b/pgLabII/EditHistoryManager/EditHistoryManager.cs @@ -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 GetHistory() => _pendingEdits.AsReadOnly(); diff --git a/pgLabII/ViewModels/QueryToolViewModel.cs b/pgLabII/ViewModels/QueryToolViewModel.cs index 3f573a3..8968d48 100644 --- a/pgLabII/ViewModels/QueryToolViewModel.cs +++ b/pgLabII/ViewModels/QueryToolViewModel.cs @@ -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 Rows { get; } = new(); - public ObservableCollection Columns { get; } = new(); + + [Reactive] private IReadOnlyList _columns = new List(); - // Commands (stubs) + // Commands public ReactiveCommand RunQuery { get; } public ReactiveCommand LoadMore { get; } public ReactiveCommand ExportResults { get; } public ReactiveCommand OpenSqlFile { get; } public ReactiveCommand 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(); + 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(); + 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(); + } + } +} + +/// +/// Dynamic row container for displaying results in the DataGrid +/// +public class RowData +{ + public RowData(int columnCount) + { + Values = new object[columnCount]; + } + //public Dictionary 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; + } } diff --git a/pgLabII/ViewModels/ServerConfigurationViewModel.cs b/pgLabII/ViewModels/ServerConfigurationViewModel.cs index b3d6135..3718e09 100644 --- a/pgLabII/ViewModels/ServerConfigurationViewModel.cs +++ b/pgLabII/ViewModels/ServerConfigurationViewModel.cs @@ -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(); }); } diff --git a/pgLabII/ViewModels/ServerListViewModel.cs b/pgLabII/ViewModels/ServerListViewModel.cs index c97369f..0e7a51b 100644 --- a/pgLabII/ViewModels/ServerListViewModel.cs +++ b/pgLabII/ViewModels/ServerListViewModel.cs @@ -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", }) diff --git a/pgLabII/ViewModels/ViewListViewModel.cs b/pgLabII/ViewModels/ViewListViewModel.cs index 01125dc..ac5d24e 100644 --- a/pgLabII/ViewModels/ViewListViewModel.cs +++ b/pgLabII/ViewModels/ViewListViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using pgLabII.Model; namespace pgLabII.ViewModels; @@ -7,9 +8,17 @@ namespace pgLabII.ViewModels; /// public class ViewListViewModel : ViewModelBase { - public ObservableCollection 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 Views { get; private set; } } diff --git a/pgLabII/Views/QueryToolView.axaml b/pgLabII/Views/QueryToolView.axaml index efb51fb..b04052d 100644 --- a/pgLabII/Views/QueryToolView.axaml +++ b/pgLabII/Views/QueryToolView.axaml @@ -7,14 +7,17 @@ mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600" x:Class="pgLabII.Views.QueryToolView" x:DataType="viewModels:QueryToolViewModel"> - +