WIP querytool
This commit is contained in:
parent
d78de23ebc
commit
fd4cb8692d
9 changed files with 317 additions and 18 deletions
|
|
@ -20,6 +20,7 @@
|
|||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AvaloniaEdit.TextMate" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
108
pgLabII/QueryToolPlan.md
Normal file
108
pgLabII/QueryToolPlan.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Features
|
||||
|
||||
- Is a part of the SingleDatabaseWindow
|
||||
- It's view should go in QueryToolView.axaml
|
||||
- Uses mvvm
|
||||
- AvaloniaEdit should be used as a query editor
|
||||
|
||||
## Editor
|
||||
|
||||
- Use CodeEditorView
|
||||
- We want to be able to open and save .sql files with the editor
|
||||
|
||||
## Result grid
|
||||
|
||||
- Use an Avalonia.Controls.DataGrid
|
||||
- The columns will change on runtime so it should be able to get the column count captions and data types from the viewmodel
|
||||
- We want to be able to sort the columns
|
||||
- We want to be able to filter the rows by defining conditions for the columns
|
||||
- We want to be able to copy the rows to the clipboard
|
||||
- We want to be able to customize cell rendering to use different colors for types and also do special things like rendering booleans as green checks and red crosses
|
||||
- Be aware results may contain many rows, we should make a decision on how to handle this
|
||||
- We want to be able to save the results to a file
|
||||
|
||||
---
|
||||
|
||||
# Step-by-step plan to create the Query Tool
|
||||
|
||||
1. Add QueryToolView to the UI shell.
|
||||
- Place the view in pgLabII\Views\QueryToolView.axaml and include it within SingleDatabaseWindow as a child region/panel. Ensure DataContext is set to QueryToolViewModel.
|
||||
- Confirm MVVM wiring: commands and properties will be bound from the ViewModel.
|
||||
|
||||
2. Integrate the SQL editor.
|
||||
- Embed AvaloniaEdit editor in the top area of QueryToolView.
|
||||
- Bind editor text to a ViewModel property (e.g., UserSql).
|
||||
- Provide commands for OpenSqlFile and SaveSqlFile; wire to toolbar/buttons and standard shortcuts (Ctrl+O/Ctrl+S).
|
||||
- Ensure file filters default to .sql and that encoding/line-endings preserve content when saving.
|
||||
|
||||
3. Add a results toolbar for query operations.
|
||||
- Buttons/controls: Run, Cancel (optional), "Load more", Auto-load on scroll toggle, Export..., and a compact status/summary text (e.g., "Showing X of Y rows").
|
||||
- Bind to RunQuery, LoadMore, ExportResults, AutoLoadMore, ResultSummary, and Status properties.
|
||||
|
||||
4. Add the result grid using Avalonia.Controls.DataGrid.
|
||||
- Enable row and column virtualization. Keep cell templates lightweight to preserve performance.
|
||||
- Start with AutoGenerateColumns=true; later switch to explicit columns if custom cell templates per type are needed.
|
||||
- Bind Items to a read-only observable collection of row objects (e.g., Rows).
|
||||
- Enable extended selection and clipboard copy.
|
||||
|
||||
5. Support dynamic columns and types from the ViewModel.
|
||||
- Expose a Columns metadata collection (names, data types, display hints) from the ViewModel.
|
||||
- On first page load, update metadata so the grid can reflect the current query’s shape.
|
||||
- If AutoGenerateColumns is disabled, construct DataGrid columns based on metadata (text, number, date, boolean with check/cross visuals).
|
||||
|
||||
6. Sorting model.
|
||||
- On column header sort request, send sort descriptor(s) to the ViewModel.
|
||||
- Re-run the query via server-side ORDER BY by wrapping the user SQL as a subquery and applying sort expressions.
|
||||
- Reset paging when sort changes (reload from page 1).
|
||||
- Clearly indicate if sorting is client-side (fallback) and only affects loaded rows.
|
||||
|
||||
7. Filtering model.
|
||||
- Provide a simple filter row/panel to define per-column conditions.
|
||||
- Convert user-entered filters to a filter descriptor list in the ViewModel.
|
||||
- Prefer server-side WHERE by wrapping the user SQL; reset paging when filters change.
|
||||
- If server-side wrapping is not possible for a given statement, apply client-side filtering to the currently loaded subset and warn that the filter is partial.
|
||||
|
||||
8. Data paging and virtualization (for 100k+ rows).
|
||||
- Choose a default page size of 1000 rows (range 500–2000).
|
||||
- On RunQuery: clear rows, reset page index, set CanLoadMore=true, fetch page 1.
|
||||
- "Load more" fetches the next page and appends. Enable infinite scroll optionally when near the end.
|
||||
- Display summary text: "Showing N of M+ rows" when total is known; otherwise "Showing N rows".
|
||||
- Consider a cap on retained rows (e.g., last 10–20k) if memory is a concern.
|
||||
|
||||
9. Query execution abstraction.
|
||||
- Use a service (e.g., IQueryExecutor) to run database calls.
|
||||
- Provide: FetchPageAsync(userSql, sort, filters, page, size, ct) and StreamAllAsync(userSql, sort, filters, ct) for export.
|
||||
- Wrap user SQL as a subquery to inject WHERE/ORDER BY/LIMIT/OFFSET safely; trim trailing semicolons.
|
||||
- Prefer keyset pagination when a stable ordered key exists.
|
||||
|
||||
10. Export/Save results.
|
||||
- Export should re-execute the query and stream the full result set directly from the database to CSV/TSV/JSON.
|
||||
- Do not export from the grid items because the grid may contain only a subset of rows.
|
||||
- Provide a Save As dialog with format choice and destination path.
|
||||
|
||||
11. Copy to clipboard and selection.
|
||||
- Enable extended row selection in the grid; support Ctrl+C to copy selected rows.
|
||||
- Provide a toolbar "Copy" button as an alternative entry point.
|
||||
|
||||
12. Status, cancellation, and errors.
|
||||
- Show progress/state (Running, Idle, Loading page k, Cancelled, Error).
|
||||
- Support cancellation tokens for long-running queries and paging operations.
|
||||
- Surface exceptions as non-blocking notifications and preserve the last successful rows.
|
||||
|
||||
13. Theming and custom cell rendering.
|
||||
- Apply subtle coloring by type (numbers, dates, strings) via cell styles or templates.
|
||||
- Render booleans as green checks/red crosses with minimal template overhead to keep virtualization effective.
|
||||
|
||||
14. Wiring in SingleDatabaseWindow.
|
||||
- Add a dedicated region/tab/panel for the Query Tool.
|
||||
- Ensure lifetime management of the QueryToolViewModel aligns with the connection/session scope.
|
||||
- Provide the active connection context/service to the ViewModel (DI or constructor).
|
||||
|
||||
15. Testing and verification.
|
||||
- Manual test: small query, large query (100k rows), sorting, filtering, load more, infinite scroll, export, copy, boolean rendering.
|
||||
- Edge cases: empty results, wide tables (many columns), slow network, cancellation mid-page, schema change between pages.
|
||||
- Performance check: scroll smoothness, memory growth under repeated paging, export throughput.
|
||||
|
||||
16. Documentation and UX notes.
|
||||
- In help/tooltip, clarify that sorting/filtering are server-side when possible; otherwise they apply only to loaded rows.
|
||||
- Show a banner when results are truncated by paging limits and how to load more.
|
||||
|
|
@ -1,22 +1,94 @@
|
|||
using System.Reactive;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace pgLabII.ViewModels;
|
||||
|
||||
public class ColumnInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? DisplayName { get; set; }
|
||||
public Type DataType { get; set; } = typeof(string);
|
||||
public bool IsSortable { get; set; } = true;
|
||||
public bool IsFilterable { get; set; } = true;
|
||||
// Add more metadata as needed (format, cell template, etc.)
|
||||
}
|
||||
|
||||
public partial class QueryToolViewModel : ViewModelBase, IViewItem
|
||||
{
|
||||
[Reactive] private string _caption = "Cap";
|
||||
// Tab caption
|
||||
[Reactive] private string _caption = "Query";
|
||||
|
||||
[Reactive] private string _query = "";
|
||||
// SQL text bound to the editor
|
||||
[Reactive] private string _userSql = "SELECT 1 AS one, 'hello' AS text;";
|
||||
|
||||
public ReactiveCommand<Unit, Unit> EditCommand { get; }
|
||||
// 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;
|
||||
|
||||
// Rows shown in the DataGrid. For now, simple object rows for AutoGenerateColumns.
|
||||
public ObservableCollection<object> Rows { get; } = new();
|
||||
public ObservableCollection<ColumnInfo> Columns { get; } = new();
|
||||
|
||||
// Commands (stubs)
|
||||
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()
|
||||
{
|
||||
EditCommand = ReactiveCommand.Create(() =>
|
||||
// Create stub commands that only update labels/rows so UI is interactive without real DB work.
|
||||
RunQuery = ReactiveCommand.Create(() =>
|
||||
{
|
||||
Query += " test";
|
||||
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;
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
this.RaisePropertyChanged(nameof(Rows)); // Force DataGrid refresh
|
||||
ResultSummary = $"Showing {Rows.Count} rows";
|
||||
Status = "Loaded more (stub)";
|
||||
// Stop after a few loads visually
|
||||
CanLoadMore = Rows.Count < 12;
|
||||
});
|
||||
|
||||
ExportResults = ReactiveCommand.Create(() =>
|
||||
{
|
||||
Status = "Export invoked (stub)";
|
||||
});
|
||||
|
||||
OpenSqlFile = ReactiveCommand.Create(() =>
|
||||
{
|
||||
Status = "Open SQL file (stub)";
|
||||
});
|
||||
|
||||
SaveSqlFile = ReactiveCommand.Create(() =>
|
||||
{
|
||||
Status = "Save SQL file (stub)";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ public class ServerConfigurationViewModel : ReactiveObject
|
|||
var window = new Views.EditServerConfigurationWindow(new(this)) { New = false };
|
||||
window.Show();
|
||||
});
|
||||
ExploreCommand = ReactiveCommand.Create(() => { /* window coordination can be injected later */ });
|
||||
ExploreCommand = ReactiveCommand.Create(() => {
|
||||
var window = new Views.SingleDatabaseWindow();
|
||||
window.Show();
|
||||
});
|
||||
}
|
||||
|
||||
public ServerConfigurationEntity Entity => _entity;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
using System.IO;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using AvaloniaEdit.Document;
|
||||
using AvaloniaEdit.TextMate;
|
||||
using TextMateSharp.Grammars;
|
||||
using System;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace pgLabII.Views.Controls;
|
||||
|
||||
|
|
@ -18,9 +21,27 @@ public partial class CodeEditorView : UserControl
|
|||
BaseCopyFilename = "",
|
||||
});
|
||||
|
||||
public static readonly AvaloniaProperty<string> TextProperty = AvaloniaProperty.Register<CodeEditorView, string>(
|
||||
nameof(Text), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => Editor?.Text ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (Editor != null && Editor.Text != value)
|
||||
Editor.Text = value ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public CodeEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.GetObservable(TextProperty).Subscribe(text =>
|
||||
{
|
||||
if (Editor != null && Editor.Text != text)
|
||||
Editor.Text = text ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
|
|
@ -32,8 +53,13 @@ public partial class CodeEditorView : UserControl
|
|||
_textMate.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions.GetLanguageByExtension(".sql").Id));
|
||||
|
||||
Editor.Document.Changed += DocumentChanged;
|
||||
Editor.TextChanged += Editor_TextChanged;
|
||||
}
|
||||
|
||||
private void Editor_TextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
SetValue(TextProperty, Editor.Text);
|
||||
}
|
||||
|
||||
private void OnSaveClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
|
|
@ -41,7 +67,6 @@ public partial class CodeEditorView : UserControl
|
|||
File.WriteAllText("final.sql", Editor.Text);
|
||||
}
|
||||
|
||||
|
||||
private void DocumentChanged(object? sender, DocumentChangeEventArgs e)
|
||||
{
|
||||
_editHistoryManager.AddEdit(e.Offset, e.InsertedText.Text, e.RemovedText.Text);
|
||||
|
|
|
|||
39
pgLabII/Views/QueryToolView.axaml
Normal file
39
pgLabII/Views/QueryToolView.axaml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:pgLabII.Views.Controls"
|
||||
xmlns:viewModels="clr-namespace:pgLabII.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600"
|
||||
x:Class="pgLabII.Views.QueryToolView"
|
||||
x:DataType="viewModels:QueryToolViewModel">
|
||||
<Grid RowDefinitions="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 3: Results toolbar -->
|
||||
<StackPanel Grid.Row="1" 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"/>
|
||||
<CheckBox Content="Auto-load on scroll" IsChecked="{Binding AutoLoadMore}" Margin="8,0,0,0"/>
|
||||
<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>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
54
pgLabII/Views/QueryToolView.axaml.cs
Normal file
54
pgLabII/Views/QueryToolView.axaml.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System.Collections.Specialized;
|
||||
using pgLabII.ViewModels;
|
||||
|
||||
namespace pgLabII.Views;
|
||||
|
||||
public partial class QueryToolView : UserControl
|
||||
{
|
||||
public QueryToolView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.DataContextChanged += (_, __) => WireColumns();
|
||||
WireColumns();
|
||||
}
|
||||
|
||||
private void WireColumns()
|
||||
{
|
||||
if (DataContext is QueryToolViewModel vm)
|
||||
{
|
||||
var grid = this.FindControl<DataGrid>("ResultsGrid");
|
||||
vm.Columns.CollectionChanged += (s, e) => RegenerateColumns(grid, vm);
|
||||
RegenerateColumns(grid, vm);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegenerateColumns(DataGrid grid, QueryToolViewModel vm)
|
||||
{
|
||||
grid.Columns.Clear();
|
||||
foreach (var col in vm.Columns)
|
||||
{
|
||||
DataGridColumn gridCol;
|
||||
if (col.DataType == typeof(bool))
|
||||
{
|
||||
gridCol = new DataGridCheckBoxColumn
|
||||
{
|
||||
Header = col.DisplayName ?? col.Name,
|
||||
Binding = new Avalonia.Data.Binding(col.Name)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
gridCol = new DataGridTextColumn
|
||||
{
|
||||
Header = col.DisplayName ?? col.Name,
|
||||
Binding = new Avalonia.Data.Binding(col.Name)
|
||||
};
|
||||
}
|
||||
grid.Columns.Add(gridCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:pgLabII.ViewModels"
|
||||
xmlns:views="clr-namespace:pgLabII.Views"
|
||||
xmlns:controls="clr-namespace:pgLabII.Views.Controls"
|
||||
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
|
|
@ -47,12 +48,7 @@
|
|||
|
||||
<TabControl.ContentTemplate>
|
||||
<DataTemplate DataType="viewModels:QueryToolViewModel">
|
||||
<DockPanel LastChildFill="True">
|
||||
<TextBox Text="{Binding Query, Mode=TwoWay}" DockPanel.Dock="Top"/>
|
||||
<Button Command="{Binding EditCommand}" DockPanel.Dock="Top">TEST</Button>
|
||||
|
||||
<controls:CodeEditorView />
|
||||
</DockPanel>
|
||||
<views:QueryToolView />
|
||||
</DataTemplate>
|
||||
</TabControl.ContentTemplate>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using pgLabII.ViewModels;
|
||||
|
||||
namespace pgLabII.Views;
|
||||
|
||||
public partial class SingleDatabaseWindow : Window
|
||||
{
|
||||
|
||||
public SingleDatabaseWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new ViewListViewModel();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue