Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public static IServiceCollection AddDemoApp(this IServiceCollection services)
services.AddXBaseDemoInfrastructure();
services.AddXBaseDemoDiagnostics();

services.AddSingleton<SchemaDesignerViewModel>();
services.AddSingleton<IndexManagerViewModel>();
services.AddSingleton<ShellViewModel>();
services.AddSingleton<MainWindow>();

Expand Down
135 changes: 135 additions & 0 deletions src/demo/XBase.Demo.App/ViewModels/IndexManagerViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using ReactiveUI;
using XBase.Demo.Domain.Services;
using XBase.Demo.Domain.Services.Models;

namespace XBase.Demo.App.ViewModels;

/// <summary>
/// Handles index create/drop operations for the selected table.
/// </summary>
public sealed class IndexManagerViewModel : ReactiveObject
{
private readonly IIndexManagementService _indexService;
private TableListItemViewModel? _table;
private string? _indexName;
private string? _indexExpression;
private string? _statusMessage;
private string? _errorMessage;
private bool _isBusy;

public IndexManagerViewModel(IIndexManagementService indexService)
{
_indexService = indexService;

CreateIndexCommand = ReactiveCommand.CreateFromTask(ExecuteCreateIndexAsync);
CreateIndexCommand.Subscribe(ApplyResult);
CreateIndexCommand.ThrownExceptions.Subscribe(OnFault);

DropIndexCommand = ReactiveCommand.CreateFromTask<IndexListItemViewModel, IndexOperationResult>(ExecuteDropIndexAsync);
DropIndexCommand.Subscribe(ApplyResult);
DropIndexCommand.ThrownExceptions.Subscribe(OnFault);

Observable.Merge(
CreateIndexCommand.IsExecuting,
DropIndexCommand.IsExecuting)
.Subscribe(isExecuting => IsBusy = isExecuting);
}

public ReactiveCommand<Unit, IndexOperationResult> CreateIndexCommand { get; }

public ReactiveCommand<IndexListItemViewModel, IndexOperationResult> DropIndexCommand { get; }

public bool IsBusy
{
get => _isBusy;
private set => this.RaiseAndSetIfChanged(ref _isBusy, value);
}

public string? IndexName
{
get => _indexName;
set => this.RaiseAndSetIfChanged(ref _indexName, value);
}

public string? IndexExpression
{
get => _indexExpression;
set => this.RaiseAndSetIfChanged(ref _indexExpression, value);
}

public string? StatusMessage
{
get => _statusMessage;
private set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}

public string? ErrorMessage
{
get => _errorMessage;
private set => this.RaiseAndSetIfChanged(ref _errorMessage, value);
}

public void SetTargetTable(TableListItemViewModel? table)
{
_table = table;
StatusMessage = null;
ErrorMessage = null;
if (table is null)
{
IndexName = null;
IndexExpression = null;
}
}

private async Task<IndexOperationResult> ExecuteCreateIndexAsync()
{
if (_table is null)
{
throw new InvalidOperationException("Select a table before creating an index.");
}

if (string.IsNullOrWhiteSpace(IndexName) || string.IsNullOrWhiteSpace(IndexExpression))
{
throw new InvalidOperationException("Index name and expression are required.");
}

var request = IndexCreateRequest.Create(_table.Model.Path, IndexName!, IndexExpression!);
return await _indexService.CreateIndexAsync(request);
}

private async Task<IndexOperationResult> ExecuteDropIndexAsync(IndexListItemViewModel index)
{
ArgumentNullException.ThrowIfNull(index);
if (_table is null)
{
throw new InvalidOperationException("Select a table before dropping an index.");
}

var request = IndexDropRequest.Create(_table.Model.Path, index.Name);
return await _indexService.DropIndexAsync(request);
}

private void ApplyResult(IndexOperationResult result)
{
if (result.Succeeded)
{
StatusMessage = result.Message;
ErrorMessage = null;
}
else
{
StatusMessage = null;
ErrorMessage = result.Message;
}
}

private void OnFault(Exception exception)
{
StatusMessage = null;
ErrorMessage = exception.Message;
}
}
33 changes: 33 additions & 0 deletions src/demo/XBase.Demo.App/ViewModels/SchemaColumnChangeViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using XBase.Demo.Domain.Schema;

namespace XBase.Demo.App.ViewModels;

/// <summary>
/// Represents an alteration action for schema preview generation.
/// </summary>
public sealed class SchemaColumnChangeViewModel
{
public SchemaColumnChangeViewModel(
ColumnChangeOperation operation,
string columnName,
SchemaColumnViewModel? columnDefinition = null,
string? newColumnName = null)
{
Operation = operation;
ColumnName = columnName ?? throw new ArgumentNullException(nameof(columnName));
ColumnDefinition = columnDefinition;
NewColumnName = newColumnName;
}

public ColumnChangeOperation Operation { get; }

public string ColumnName { get; }

public SchemaColumnViewModel? ColumnDefinition { get; }

public string? NewColumnName { get; }

public ColumnChangeDefinition ToDefinition()
=> new(Operation, ColumnName, ColumnDefinition?.ToDefinition(), NewColumnName);
}
42 changes: 42 additions & 0 deletions src/demo/XBase.Demo.App/ViewModels/SchemaColumnViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using XBase.Demo.Domain.Schema;

namespace XBase.Demo.App.ViewModels;

/// <summary>
/// Represents a column definition used by the schema designer.
/// </summary>
public sealed class SchemaColumnViewModel
{
public SchemaColumnViewModel(string name, string dataType, bool allowNulls = true, int? length = null, int? scale = null)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Column name is required.", nameof(name));
}

if (string.IsNullOrWhiteSpace(dataType))
{
throw new ArgumentException("Column data type is required.", nameof(dataType));
}

Name = name;
DataType = dataType;
AllowNulls = allowNulls;
Length = length;
Scale = scale;
}

public string Name { get; }

public string DataType { get; }

public bool AllowNulls { get; }

public int? Length { get; }

public int? Scale { get; }

public TableColumnDefinition ToDefinition()
=> new(Name, DataType, AllowNulls, Length, Scale);
}
165 changes: 165 additions & 0 deletions src/demo/XBase.Demo.App/ViewModels/SchemaDesignerViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using ReactiveUI;
using XBase.Demo.Domain.Schema;
using XBase.Demo.Domain.Services;

namespace XBase.Demo.App.ViewModels;

/// <summary>
/// Provides commands for generating schema DDL previews.
/// </summary>
public sealed class SchemaDesignerViewModel : ReactiveObject
{
private readonly ISchemaDdlService _ddlService;
private readonly ObservableCollection<SchemaColumnViewModel> _columns = new();
private readonly ObservableCollection<SchemaColumnChangeViewModel> _alterations = new();
private string? _tableName;
private string? _previewOperation;
private string _previewUpScript = string.Empty;
private string _previewDownScript = string.Empty;
private string? _errorMessage;

public SchemaDesignerViewModel(ISchemaDdlService ddlService)
{
_ddlService = ddlService;

Columns = new ReadOnlyObservableCollection<SchemaColumnViewModel>(_columns);
Alterations = new ReadOnlyObservableCollection<SchemaColumnChangeViewModel>(_alterations);

GenerateCreatePreviewCommand = ReactiveCommand.CreateFromTask(ExecuteGenerateCreatePreviewAsync);
GenerateCreatePreviewCommand.Subscribe(ApplyPreview);
GenerateCreatePreviewCommand.ThrownExceptions.Subscribe(OnPreviewFault);

GenerateAlterPreviewCommand = ReactiveCommand.CreateFromTask(ExecuteGenerateAlterPreviewAsync);
GenerateAlterPreviewCommand.Subscribe(ApplyPreview);
GenerateAlterPreviewCommand.ThrownExceptions.Subscribe(OnPreviewFault);

GenerateDropPreviewCommand = ReactiveCommand.CreateFromTask(ExecuteGenerateDropPreviewAsync);
GenerateDropPreviewCommand.Subscribe(ApplyPreview);
GenerateDropPreviewCommand.ThrownExceptions.Subscribe(OnPreviewFault);
}

public ReadOnlyObservableCollection<SchemaColumnViewModel> Columns { get; }

public ReadOnlyObservableCollection<SchemaColumnChangeViewModel> Alterations { get; }

public ReactiveCommand<Unit, DdlPreview> GenerateCreatePreviewCommand { get; }

public ReactiveCommand<Unit, DdlPreview> GenerateAlterPreviewCommand { get; }

public ReactiveCommand<Unit, DdlPreview> GenerateDropPreviewCommand { get; }

public string? TableName
{
get => _tableName;
set => this.RaiseAndSetIfChanged(ref _tableName, value);
}

public string? PreviewOperation
{
get => _previewOperation;
private set => this.RaiseAndSetIfChanged(ref _previewOperation, value);
}

public string PreviewUpScript
{
get => _previewUpScript;
private set => this.RaiseAndSetIfChanged(ref _previewUpScript, value);
}

public string PreviewDownScript
{
get => _previewDownScript;
private set => this.RaiseAndSetIfChanged(ref _previewDownScript, value);
}

public string? ErrorMessage
{
get => _errorMessage;
private set => this.RaiseAndSetIfChanged(ref _errorMessage, value);
}

public void SetTargetTable(TableListItemViewModel? table)
{
TableName = table?.Name;
ErrorMessage = null;
}

public void SetColumns(params SchemaColumnViewModel[] columns)
{
_columns.Clear();
foreach (var column in columns)
{
_columns.Add(column);
}
}

public void SetAlterations(params SchemaColumnChangeViewModel[] alterations)
{
_alterations.Clear();
foreach (var alteration in alterations)
{
_alterations.Add(alteration);
}
}

private async Task<DdlPreview> ExecuteGenerateCreatePreviewAsync()
{
if (string.IsNullOrWhiteSpace(TableName))
{
throw new InvalidOperationException("Table name is required to generate DDL.");
}

if (_columns.Count == 0)
{
throw new InvalidOperationException("At least one column is required to generate a CREATE TABLE script.");
}

var schema = new TableSchemaDefinition(TableName!, _columns.Select(column => column.ToDefinition()).ToArray());
var preview = await _ddlService.BuildCreateTablePreviewAsync(schema);
return preview;
}

private async Task<DdlPreview> ExecuteGenerateAlterPreviewAsync()
{
if (string.IsNullOrWhiteSpace(TableName))
{
throw new InvalidOperationException("Table name is required to generate DDL.");
}

if (_alterations.Count == 0)
{
throw new InvalidOperationException("At least one alteration is required to generate an ALTER TABLE script.");
}

var definition = new TableAlterationDefinition(TableName!, _alterations.Select(change => change.ToDefinition()).ToArray());
return await _ddlService.BuildAlterTablePreviewAsync(definition);
}

private async Task<DdlPreview> ExecuteGenerateDropPreviewAsync()
{
if (string.IsNullOrWhiteSpace(TableName))
{
throw new InvalidOperationException("Table name is required to generate DDL.");
}

return await _ddlService.BuildDropTablePreviewAsync(TableName!);
}

private void ApplyPreview(DdlPreview preview)
{
PreviewOperation = preview.Operation;
PreviewUpScript = string.Join(Environment.NewLine + Environment.NewLine, preview.UpStatements);
PreviewDownScript = string.Join(Environment.NewLine + Environment.NewLine, preview.DownStatements);
ErrorMessage = null;
}

private void OnPreviewFault(Exception exception)
{
ErrorMessage = exception.Message;
}
}
Loading