qleany 1.7.3

Architecture generator for Rust and C++/Qt applications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
# How Operations Flow

This document outlines the data flow within a Qleany-generated application, detailing how data is processed and transferred between components. Both the **C++/Qt** and **Rust** targets follow the same architecture, with language-appropriate implementations. If you're lost in the generated code, start here.

## The Big Picture

Every operation in a Qleany application, whether creating a `Calendar`, updating a `CalendarEvent`, or running a custom feature use case, follows the same pipeline:

**UI → Controller → Use Case → Unit of Work → Repository → Table → Database**

And on the way back:

**Database → Table → Repository (events queued) → Unit of Work → Use Case (produces return DTO) → Controller → UI receives result. Events are flushed.**

The key invariant: **events are never sent until the transaction commits**. If anything goes wrong, the transaction rolls back, the events are discarded, and the UI never sees a thing. No half-baked state, no confused models, no fun debugging sessions at 2 AM.

## Commands

Commands are operations that modify data. There are two flavors:

- **Undoable commands**: modify entities marked `undoable: true` in the manifest. They are executed through the undo/redo system, which keeps them on a named stack for later undo/redo. Each use case captures enough state to reverse itself.

- **Not-undoable commands**: same machinery, same pipeline, but they live on a dedicated throwaway stack (size 1 in C++/Qt, `stack_id: None` maps to global stack 0 in Rust). After execution, the stack is cleared. You get the transactional safety without the history.

### C++/Qt Command Flow

Let's trace a scalar-only update of a `Calendar` entity, from button press to UI refresh. (For updates that also modify relationships, use `updateWithRelationships` — same flow but writes junction tables in step 3c.)

```
── Controller setup ──────────────────────────────────────────────

1.  UI action
2.  CalendarController::update(QList<UpdateCalendarDto>)
      2a. creates CalendarUnitOfWork (owns DbSubContext + SignalBuffer)
      2b. creates UpdateCalendarUseCase (owns the UoW)
      2c. wraps the use case in an UndoRedoCommand
      2d. co_awaits UndoRedoSystem::executeCommandAsync()

── Worker thread (inside the undo/redo system) ───────────────────

3.  UseCase::execute(calendars)

      3a. UoW::beginTransaction()
            DbSubContext begins SQLite transaction
            SignalBuffer starts buffering

      3b. UoW::get(ids)
            fetch originals for undo

      3c. UoW::update(entities)                        // scalar fields only
            Repository::update()
                Table::updateMany()                  // writes entity table, NOT junction tables
            Repository::emitUpdated(ids)
                SignalBuffer::push(callback)       // queued, not delivered yet

      3d. UoW::commit()
            DbSubContext commits SQLite transaction
            SignalBuffer::flush()                   // NOW the events fire
                CalendarEvents::publishUpdated(ids) via Qt::QueuedConnection

── Return to controller ──────────────────────────────────────────

4.  UseCase returns QList<CalendarDto>
5.  Command pushed onto undo stack
6.  co_return result to UI
```

The use case stores the original entities before updating them. On `undo()`, it replays the originals. On `redo()`, it replays the updated values. Each undo/redo opens its own transaction and flushes its own signal buffer.

The `SignalBuffer` is the mechanism for **deferred events**. It sits between the repository and Qt's signal system. During a transaction, `emitCreated/Updated/Removed` calls don't emit signals directly. Instead, they push callbacks into the buffer. On `commit()`, the buffer flushes  (=sends) all callbacks. On `rollback()`, it discards them. Simple, effective, and prevents the UI from seeing phantom state from a failed transaction.

Commands are **asynchronous** thanks to QCoro coroutines. The controller `co_await`s the undo/redo system, which does the actual work on its thread and signals back when done. The UI thread is never blocked. But coroutines are cooperative, and this matters: if your use case does CPU-intensive work inside `execute()`, the coroutine won't magically make it non-blocking. That's what long operations are for (see below).

### Rust Command Flow

Same architecture, different execution model. Rust is **synchronous**:

```
── Controller setup ──────────────────────────────────────────────

1.  UI action
2.  calendar_controller::update(db_context, event_hub, undo_redo_manager, stack_id, &update_dto)
      2a. creates CalendarUnitOfWorkFactory
      2b. creates UpdateCalendarUseCase (owns the factory)

── Execution (same thread, synchronous) ──────────────────────────

3.  uc.execute(&dto)

      3a. uow = factory.create()
      3b. uow.begin_transaction()
            write transaction begins

      3c. uow.get_calendar(id)
            fetch original for undo

      3d. uow.update_calendar(&entity)                   // scalar fields only
            CalendarRepository::update(event_buffer, &entity)
                CalendarTable::update(&entity)            // writes entity table, NOT junction tables
                event_buffer.push(Calendar(Updated))     // queued, not delivered yet

      3e. uow.commit()
            transaction committed
            EventBuffer::flush()                          // NOW the events fire
                event_hub.send_event(Calendar(Updated)) via flume channel

── Return to controller ──────────────────────────────────────────

4.  undo_redo_manager.add_command_to_stack(Box::new(uc), stack_id)
5.  returns CalendarDto
```

In Rust, events are deferred via an `EventBuffer` owned by each write unit of work. Repositories push events into the buffer during a transaction. On `commit()`, the buffer flushes (=sends) all events to the central `EventHub` (a flume channel). On `rollback()`, the buffer is discarded. The event loop runs on a dedicated thread, receiving events from the hub and pushing them into a shared `Queue` (`Arc<Mutex<Vec<Event>>>`). The UI polls this queue to pick up changes.

The `UndoRedoManager` is simpler than the C++/Qt version: no async, no worker thread. Commands implement the `UndoRedoCommand` trait (`undo()`, `redo()`, `as_any()`), and the manager maintains multiple stacks with `HashMap<u64, StackData>`. Each stack has an undo and redo `Vec`. The manager also supports composite commands for grouping multiple operations as one undoable unit (via `begin_composite()` / `end_composite()` / `cancel_composite()`), and command merging for operations like continuous typing. `begin_composite()` returns `Result<()>` and fails if a composite is already in progress for a different stack. `cancel_composite()` undoes any already-executed sub-commands before clearing the composite state.

The key difference: in C++/Qt, the undo/redo system *executes* the command. In Rust, the use case executes first, then the resulting command object is *pushed* to the undo/redo stack. Same result, different choreography.

## Queries

Queries only read data. They never modify state, so they don't need undo/redo history.

### C++/Qt

Queries still go through the undo/redo system, not for undo, but for **serialization**. The system guarantees that queries execute between commands, never concurrently with one. This prevents dirty reads.

```cpp
QCoro::Task<QList<CalendarDto>> CalendarController::get(const QList<int> &calendarIds) const
{
    co_return co_await Helpers::executeReadQuery<QList<CalendarDto>>(
        m_undoRedoSystem,
        u"Get calendars Query"_s,
        [this, calendarIds]() -> QList<CalendarDto> {
            auto uow = std::make_unique<CalendarUnitOfWork>(*m_dbContext, m_eventRegistry);
            auto useCase = std::make_unique<GetUC>(std::move(uow));
            return useCase->execute(calendarIds);
        });
}
```

The query lambda creates its own UoW and use case, executes synchronously inside the undo/redo system's thread, and returns the result. No events, no signal buffer, no undo stack.

### Rust

Queries use a **read-only unit of work** (`CalendarReadUoW`) that opens a read transaction on the in-memory store. No event hub is needed, no undo manager involved.

```rust
pub fn get(db_context: &DbContext, id: &EntityId) -> Result<Option<CalendarDto>> {
    let uow_factory = CalendarReadUoWFactory::new(db_context);
    let uc = use_cases::GetUseCase::new(uow_factory);
    Ok(uc.execute(id)?.map(|e| e.into()))
}
```

Straightforward. The read transaction provides a consistent snapshot of the data.

## Feature Use Cases

Feature use cases are the custom business logic defined in the `features:` section of the manifest. They look like entity CRUD use cases, but with one important difference: **each feature use case gets its own unit of work**. Entity CRUD use cases within `direct_access` share a unit of work per entity. Feature use cases don't share. Each one is self-contained with access to whichever repositories it needs.

### C++/Qt

Feature use cases that are not long operations follow the same command or query patterns as entity CRUD. For example, `get_upcoming_reminders` (which is `read_only: true` and not a long operation) executes as a read query through the undo/redo system:

```cpp
QCoro::Task<UpcomingRemindersDto> CalendarManagementController::getUpcomingReminders(
    const GetUpcomingRemindersDto &dto)
{
    co_return co_await Common::ControllerHelpers::executeReadQuery<UpcomingRemindersDto>(
        m_undoRedoSystem,
        u"get_upcoming_reminders Query"_s,
        [this, dto]() -> UpcomingRemindersDto {
            auto uow = std::make_unique<GetUpcomingRemindersUnitOfWork>(
                *m_dbContext, m_eventRegistry, m_featureEventRegistry);
            auto useCase = std::make_shared<GetUpcomingRemindersUseCase>(std::move(uow));
            return useCase->execute(dto);
        });
}
```

Feature use cases also have their own **event registry** (`FeatureEventRegistry` / `CalendarManagementEvents`), separate from the entity event registry. This keeps entity-level events (Calendar created, updated, removed) distinct from feature-level events (GetEventsInRange completed). The UI can subscribe to exactly what it cares about.

### Rust

Same pattern. Non-long-operation feature use cases execute directly. The controller delegates to the use case, which emits the feature event internally via the UoW:

```rust
// Controller — no event sending here, it's handled by the use case
pub fn get_upcoming_reminders(
    db_context: &DbContext,
    event_hub: &Arc<EventHub>,
    dto: &GetUpcomingRemindersDto,
) -> Result<UpcomingRemindersDto> {
    let uow_context = GetUpcomingRemindersUnitOfWorkFactory::new(db_context, event_hub);
    let mut uc = GetUpcomingRemindersUseCase::new(Box::new(uow_context));
    let return_dto = uc.execute(dto)?;
    Ok(return_dto)
}

// Inside the use case's execute(), after commit:
uow.publish_get_upcoming_reminders_event(vec![], None);

// The UoW implementation sends the event directly:
fn publish_get_upcoming_reminders_event(&self, ids: Vec<EntityId>, data: Option<String>) {
    self.event_hub.send_event(Event {
        origin: Origin::CalendarManagement(GetUpcomingReminders),
        ids,
        data,
    });
}
```

This mirrors the C++/Qt pattern where the UoW publishes the signal (`m_uow->publishGetUpcomingRemindersSignal()`). The `ids` and `data` parameters let the use case attach context to the event.

In Rust, there's no separate feature event registry. Entity events and feature events all flow through the same `EventHub` with an `Origin` enum that discriminates between `DirectAccess(Calendar(Updated))` and `CalendarManagement(GetUpcomingReminders)`. One hub, one queue, one subscription point.

## Long Operations

Long operations are those that take a long time to complete. Yes, the name is self-explanatory. I'm proud of this.

Typical examples include big database operations, heavy network requests, file generation (like Qleany's own code generation), or any task where you want a progress bar and a cancel button.

A use case marked `long_operation: true` in the manifest gets a completely different controller API:

```
run_[use_case_name](DtoIn)      → returns an operation ID (string)
get_[use_case_name]_progress(id) → returns progress (percentage + message)
get_[use_case_name]_result(id)   → returns the output DTO
```

To cancel, call `cancel_operation(id)` on the long operation manager.

### C++/Qt

Long operations bypass the coroutine pipeline entirely. The controller creates the use case, hands it to the `LongOperationManager`, which runs it on a background thread via `QtConcurrent::run`:

```cpp
QString CalendarManagementController::getEventsInRange(const GetEventsInRangeDto &dto)
{
    auto uow = std::make_unique<GetEventsInRangeUnitOfWork>(
        *m_dbContext, m_eventRegistry, m_featureEventRegistry);
    auto operation = std::make_shared<GetEventsInRangeUseCase>(std::move(uow), dto);
    return m_longOperationManager->startOperation(std::move(operation));
}
```

The controller returns the operation ID **synchronously** (no `co_await`). The UI then polls `getGetEventsInRangeProgress(operationId)` to update a progress bar, and calls `getGetEventsInRangeResult(operationId)` when done to retrieve the result DTO (deserialized from JSON internally).

The `LongOperationManager` emits Qt signals (`progressChanged`, `operationCompleted`, `operationFailed`, `operationCancelled`) so the UI can also use signal/slot connections instead of polling.

### Rust

Since Rust is synchronous, long operations run on a **spawned thread**. The operation implements the `LongOperation` trait:

```rust
pub trait LongOperation: Send + 'static {
    type Output: Send + Sync + 'static + serde::Serialize;

    fn execute(
        &self,
        progress_callback: Box<dyn Fn(OperationProgress) + Send>,
        cancel_flag: Arc<AtomicBool>,
    ) -> Result<Self::Output>;
}
```

The `LongOperationManager` spawns a thread, passes in a progress callback and a cancel flag, and manages status tracking through `Arc<Mutex<...>>` shared state. The result is serialized to JSON and stored for later retrieval.

Progress events flow through the `EventHub` with `Origin::LongOperation(Progress/Completed/Failed/Cancelled)`, carrying the operation ID and progress data as serialized JSON in the `data` field.

### Scenarios

Long operations are not undoable. What happens around them depends on what they touch:

**The operation modifies undoable entities** (e.g., bulk-importing events into a calendar): clear the impacted undo stacks after the operation completes, or all of them if you're feeling cautious. The entity events fire on success, the UI refreshes, and the user starts with a clean undo history. Trying to interleave a long operation with existing undo history is asking for trouble.

**The operation modifies non-undoable entities** (e.g., updating cache or search indices): nothing special. Entity events fire on success, the UI picks them up.

**The operation only reads entities and produces output** (`read_only: true`): this is the "generate files" pattern. Qleany's own file generation is a long operation that reads entities from the internal database, writes files to disk, and reports progress. It never modifies the database. "Read-only" means read-only *with respect to entities*. It can write files, call APIs, whatever it needs.

**The operation crunches data and returns results for user approval** (`read_only: true`): think "search & replace across 2000 files." The long operation finds all matches and returns a preview. The user reviews, then a second use case applies the accepted changes. That second step can be a regular undoable use case if you want the user to be able to revert it.

## Events

Events are the backbone of UI reactivity. When a `Calendar` is updated, the UI needs to know. Not "eventually," not "when it feels like it," but precisely when the transaction commits and never before.

### C++/Qt

Entity events and feature events live in separate registries:

- **Entity events**: Each entity has a dedicated `[Entity]Events` class (e.g., `CalendarEvents`) with signals: `created(QList<int>)`, `updated(QList<int>)`, `removed(QList<int>)`, and `relationshipChanged(int, RelationshipField, QList<int>)`. These are centralized in `EventRegistry`, which also provides `errorOccurred(commandName, errorMessage)` for command failures.

- **Feature events**: Each feature group has a `[Feature]Events` class (e.g., `CalendarManagementEvents`) with a signal per use case. Centralized in `FeatureEventRegistry`, which also provides `errorOccurred(commandName, errorMessage)`.

Both registries forward their `errorOccurred` signal to `ServiceLocator::errorOccurred`, giving the UI a single subscription point for all command errors.

Events are **deferred** via the `SignalBuffer`. The flow:

1. Repository calls `emitUpdated(ids)`.
2. `SignalBuffer::push()` captures the callback (it's a lambda wrapping `QMetaObject::invokeMethod` with `Qt::QueuedConnection`).
3. On `commit()`, `SignalBuffer::flush()` executes all callbacks.
4. On `rollback()`, `SignalBuffer::discard()` drops them all.

The `Qt::QueuedConnection` ensures signals are delivered on the events object's thread (typically the main thread), not the worker thread where the command executed. Cross-thread signal delivery is handled by Qt's meta-object system, with metatypes registered at construction time.

### Rust

No separate registries here. Entity events, feature events, undo/redo events, long operation events,they all flow through a single `EventHub`:

```rust
pub struct Event {
    pub origin: Origin,      // which subsystem produced this
    pub ids: Vec<EntityId>,  // affected entity IDs
    pub data: Option<String>, // optional JSON payload
}

pub enum Origin {
    DirectAccess(DirectAccessEntity),  // Calendar(Created), Tag(Updated), ...
    UndoRedo(UndoRedoEvent),           // Undone, Redone, ...
    LongOperation(LongOperationEvent), // Started, Progress, Completed, ...
    CalendarManagement(CalendarManagementEvent), // one variant per feature group
    // ... additional feature groups from the manifest ...
}
```

The `EventHub` uses a flume channel internally. Events are sent from any thread via `send_event()`, received by a dedicated event loop thread, and pushed into a shared `Queue` (`Arc<Mutex<Vec<Event>>>`). The UI polls this queue to pick up changes. One hub, one queue, one subscription point. The `Origin` enum tells you who sent what.

Events are **deferred** via the `EventBuffer`, the Rust equivalent of the C++/Qt `SignalBuffer`. Each write unit of work owns one (wrapped in `RefCell` for single-threaded UoWs, `Mutex` for long-operation UoWs). The flow:

1. Repository calls `event_buffer.push(event)`.
2. The buffer holds it in a `Vec<Event>`. Not delivered yet.
3. On `commit()`, the UoW calls `event_buffer.flush()`, drains all pending events, and sends each one to the `EventHub`.
4. On `rollback()`, the UoW calls `event_buffer.discard()`. Gone. The UI never knows.

```rust
pub struct EventBuffer {
    buffering: bool,
    pending: Vec<Event>,
}
```

Deliberately simple. `begin_buffering()` arms it and clears stale events from a previous cycle. `push()` queues an event (silently dropped if not buffering). `flush()` drains via `std::mem::take()` and hands you back the `Vec`. `discard()` clears everything and stops buffering.

One edge case worth knowing: `restore_to_savepoint()` discards the buffer (the database state it described is gone), then sends a `Reset` event **directly** to the `EventHub`, bypassing the buffer entirely. The UI must refresh immediately, that Reset cannot sit around waiting for a future `commit()`.

Thread safety lives at the UoW level, not the repository level. The repositories just take `&mut EventBuffer`.

## Transaction Boundaries

Both targets use transactions to guarantee atomicity:

- **C++/Qt**: SQLite transactions with WAL mode. The `DbSubContext` manages `BEGIN/COMMIT/ROLLBACK`. Savepoints are available in the API just in case the developer really needs them, but Qleany doesn't use them internally (see below). Snapshots are better.

- **Rust**: in-memory `im::HashMap` store (persistent data structure with structural sharing). The `Transaction` struct wraps a shared `Arc<HashMapStore>`. `begin_write_transaction()` automatically creates a savepoint (O(1) thanks to `im::HashMap`). Mutations are applied immediately to the store. `commit()` discards the savepoint, making mutations permanent. `rollback()` restores the savepoint, undoing all mutations. If the transaction is dropped without commit or rollback, `Drop` restores the savepoint as a safety net. Additional explicit savepoints are available via `create_savepoint()` / `restore_to_savepoint()`.

In both cases, the unit of work owns the transaction lifecycle. `beginTransaction()` opens it (and arms the event buffer), `commit()` closes it successfully (and flushes buffered events), `rollback()` aborts it (and discards buffered events).

### Why Snapshots, Not Savepoints

Early versions of Qleany used database savepoints to handle undo for destructive operations. This turned out to be a trap: savepoints restore *everything*, including non-undoable data. Now, `create`, `createOrphans`, `remove`, `setRelationshipIds`, and `moveRelationshipIds` use **cascading table-level snapshots** that only touch the affected entities.

For the full story, see the [Undo-Redo Architecture](undo-redo-architecture.md#savepoints) documentation.

## Error Control Flow

This section describes what happens when things go wrong: a repository call throws, a transaction fails to commit, an undo operation blows up. Both targets follow the same principle -- **failed operations must leave no observable trace** (no events emitted, no stale undo history, no half-committed data) -- but the mechanics differ.

### C++/Qt

#### Use case level: try/catch + explicit rollback

Every generated use case method (`execute()`, `undo()`, `redo()`) wraps its work in the same pattern:

```cpp
try
{
    if (!m_uow->beginTransaction())
        throw std::runtime_error("Failed to begin transaction");

    // ... repository calls ...

    if (!m_uow->commit())
        throw std::runtime_error("Failed to commit transaction");
}
catch (...)
{
    m_uow->rollback();
    throw;
}
```

`beginTransaction()` and `commit()` return `bool`. A `false` return is promoted to an exception so it enters the `catch(...)` block. In the catch block, `rollback()` calls `SignalBuffer::discard()`, which drops all queued entity events. Then the exception is re-thrown.

On successful `commit()`, the `UnitOfWorkBase` calls `SignalBuffer::flush()`, which delivers all queued events. If `commit()` returns `false`, the UoW itself calls `discard()` before returning. Either way, the invariant holds: **events fire if and only if the transaction commits**.

```cpp
// UnitOfWorkBase (uow_base.h)
bool commit() override
{
    bool ok = m_dbSubContext.commit();
    if (ok)
        m_signalBuffer->flush();     // success: deliver all events
    else
        m_signalBuffer->discard();   // commit failed: drop all events
    return ok;
}
bool rollback() override
{
    m_dbSubContext.rollback();
    m_signalBuffer->discard();       // rollback: drop all events
    return true;
}
```

#### UndoRedoCommand: exceptions become Result values

Use cases execute on a background thread via `QtConcurrent::run`. The `UndoRedoCommand` wraps each call in a try/catch:

```cpp
auto future = QtConcurrent::run([safeThis, executeFunction]() -> Result<void> {
    try
    {
        QPromise<Result<void>> promise;
        executeFunction(promise);     // calls useCase->execute()
        return Result<void>();
    }
    catch (const std::exception &e)
    {
        return Result<void>(QString::fromStdString(e.what()), ErrorCategory::ExecutionError);
    }
    catch (...)
    {
        return Result<void>("Unknown exception"_L1, ErrorCategory::UnknownError);
    }
});
```

The exception thrown by the use case (after it has already rolled back its own transaction) is caught here and converted to a `Result<void>`. When the future completes, `onExecuteFinished()` (or `onUndoFinished()` / `onRedoFinished()`) checks the result and emits `finished(bool success)`. This signal is what the undo/redo stack and the controller coroutine both listen to.

#### Undo/redo stack: failure recovery

The `UndoRedoStack` moves commands between stacks *before* the async operation runs. On failure, `onCommandFinished(false)` restores the stacks:

- **Execute fails:** The command was left at the top of `m_undoStack` (it was already pushed there). On failure, the stack **pops and drops it**. The command is gone, a failed execute should leave no trace.

- **Undo fails:** The command was moved from `m_undoStack` to `m_redoStack` before `asyncUndo()`. On failure, the command is **moved back from redo to undo**, restoring the stack to its pre-undo state. The use case's catch block already rolled back the transaction, so the database is unchanged. The user can retry the undo.

- **Redo fails:** The command was moved from `m_redoStack` to `m_undoStack` before `asyncRedo()`. On failure, the stack **pops and drops it**. Same as execute failure.

```cpp
void UndoRedoStack::onCommandFinished(bool success)
{
    if (!success)
    {
        if (!m_redoStack.isEmpty() && m_redoStack.top() == m_currentCommand)
        {
            // Undo failed: move command back from redo to undo stack
            auto cmd = m_redoStack.pop();
            m_undoStack.push(cmd);
        }
        else if (!m_undoStack.isEmpty() && m_undoStack.top() == m_currentCommand)
        {
            // Execute or redo failed: drop command from undo stack
            m_undoStack.pop();
        }
    }
    m_currentCommand.reset();
    updateState();
    Q_EMIT commandFinished(success);
}
```

#### Controller level: defaults on failure + error signals

The controller coroutine `co_await`s the undo/redo system with a timeout. Each command helper takes an `onError` callback that the controller wires to the appropriate event registry:

```cpp
std::optional<bool> success = co_await undoRedoSystem->executeCommandAsync(
    command, timeoutMs, undoRedoStackId);

if (!success.has_value()) [[unlikely]]        // timeout
{
    QString msg = commandName + " timed out"_L1;
    qWarning() << msg;
    if (onError) onError(commandName, msg);   // signal-based error reporting
    co_return ResultT{};                      // default-constructed result
}
if (!success.value()) [[unlikely]]            // execution failed
{
    QString msg = "Failed to execute "_L1 + commandName;
    qWarning() << msg;
    if (onError) onError(commandName, msg);   // signal-based error reporting
    co_return ResultT{};                      // default-constructed result
}
co_return result;                             // success
```

On timeout or failure, the controller returns a default-constructed result (empty list, default DTO) *and* invokes the `onError` callback. The callback is a lambda that calls `publishError()` on the appropriate event registry (`EventRegistry` for entity controllers, `FeatureEventRegistry` for feature controllers), which emits `errorOccurred(commandName, errorMessage)`. Both registries forward this signal to `ServiceLocator::errorOccurred`, giving the UI a single subscription point for all command errors:

```
Controller onError lambda
    → EventRegistry::publishError() / FeatureEventRegistry::publishError()
        → errorOccurred signal
            → ServiceLocator::errorOccurred signal  (connected in setters)
```

The return value stays simple (default-constructed) so the controller API remains easy to use from QML. The `errorOccurred` signal provides the structured error details for UIs that need to display error messages, show toasts, or log failures.

#### Long operations: failure via signals

Long operations run on a `QtConcurrent::run` thread. If `ILongOperation::execute()` throws, the `QFutureWatcher::finished` handler catches it:

```cpp
try {
    const QJsonObject result = watcher->result();  // re-throws if execute() threw
    m_completedResults.insert(operationId, result);
    Q_EMIT operationCompleted(operationId, result);
}
catch (const std::exception &e) {
    Q_EMIT operationFailed(operationId, QString::fromUtf8(e.what()));
}
```

On failure, `operationFailed(operationId, errorMessage)` is emitted. No result is stored. The controller's `get_*_result()` returns `std::nullopt` in that case. On QML, it will be seen, as an invalid QVariant. The UI must listen to the `operationFailed` signal or poll `getResult()` and handle `nullopt`.

### Rust

#### Use case level: `?` operator + implicit rollback via Drop

Rust use cases use the `?` operator throughout. Any failure causes an immediate `Err` return:

```rust
pub fn execute(&mut self, dto: &CalendarDto) -> Result<CalendarDto> {
    let mut uow = self.uow_factory.create();
    uow.begin_transaction()?;               // fails? Err returned, uow dropped
    if uow.get_calendar(&dto.id)?.is_none() {
        return Err(anyhow!("..."));         // uow dropped without commit
    }
    let old_entity = uow.get_calendar(&dto.id)?.unwrap();
    let entity = uow.update_calendar(&dto.into())?;
    uow.commit()?;                          // fails? Err returned, but transaction
                                            //   was already consumed by commit()

    // only reached on full success:
    self.undo_stack.push_back(old_entity);
    Ok(entity.into())
}
```

There is **no explicit rollback** in the error path. If the use case returns `Err`, the `?` operator propagates the error and the `Transaction` is dropped without `commit()`. The `Drop` implementation automatically restores the auto-savepoint, undoing all partial mutations. The `EventBuffer` is similarly safe: if it is dropped without `flush()`, the buffered events are simply freed. No events are ever delivered for uncommitted work.

The undo stack push happens **after** `commit()`. If any step before commit fails, the old entity is never stored in the undo stack and is simply dropped with the local variable. This prevents stale entries from accumulating on failed operations.

#### Controller level: `?` propagation

The controller uses the `?` operator to chain the use case execution and the undo/redo registration:

```rust
pub fn update(
    db_context: &DbContext,
    event_hub: &Arc<EventHub>,
    undo_redo_manager: &mut UndoRedoManager,
    stack_id: Option<u64>,
    entity: &CalendarDto,
) -> Result<CalendarDto> {
    let uow_factory = CalendarUnitOfWorkFactory::new(db_context, event_hub);
    let mut uc = UpdateCalendarUseCase::new(Box::new(uow_factory));
    let result = uc.execute(entity)?;                                    // fails? uc dropped
    undo_redo_manager.add_command_to_stack(Box::new(uc), stack_id)?;     // fails? mutation committed
                                                                         //   but not in undo history
    Ok(result)
}
```

If `execute()` fails, the use case is dropped and never added to the undo stack. If `execute()` succeeds but `add_command_to_stack()` fails (e.g., invalid stack ID), the mutation is committed to the database but the command is not tracked. The caller gets an `Err`, which is misleading since the data change persisted. In practice this edge case does not arise because stack IDs are set up at initialization time.

#### Undo/redo manager: pop-then-try, re-push on failure

The `UndoRedoManager` pops the command from the stack *before* running `undo()` or `redo()`. If the operation fails, the command is re-pushed to its original stack to preserve it for retry:

```rust
pub fn undo(&mut self, stack_id: Option<u64>) -> Result<()> {
    // ...
    if let Some(mut command) = stack.undo_stack.pop() {
        if let Err(e) = command.undo() {
            log::error!("Undo failed, re-pushing command: {e}");
            stack.undo_stack.push(command);   // preserve for retry
            return Err(e);
        }
        stack.redo_stack.push(command);   // only on success
    }
    Ok(())
}
```

This matches C++/Qt behavior: a failed undo moves the command back to the undo stack for retry. The rationale: the store snapshot was not applied, so the store is in the pre-undo state. The command's internal state remains valid. Re-pushing allows the user to retry the operation.

The `redo()` path is symmetric: on failure, the command is re-pushed to the redo stack.

#### Composite commands: partial rollback on cancel, re-push on failure

`CompositeCommand::undo()` iterates sub-commands in reverse with `?`:

```rust
fn undo(&mut self) -> Result<()> {
    for command in self.commands.iter_mut().rev() {
        command.undo()?;   // short-circuits on first failure
    }
    Ok(())
}
```

If commands [A, B, C] are being undone in order C, B, A and B fails: C's undo has already committed (each sub-command opens its own transaction). A's undo never runs. The composite is in a **partially undone state**. Since the `UndoRedoManager` then re-pushes the entire composite to its original stack, the user can retry.

**Cancelling a composite in progress** (via `cancel_composite()`) undoes any already-executed sub-commands in reverse order before clearing the composite state. This ensures the database returns to its pre-composite state even if the composite was only partially built.

In practice, composites group closely related operations (e.g., create entity + set relationship) where failure of one implies the other would also fail.

#### Long operations: status enum + event

When a long operation's `execute()` returns `Err`, the manager sets the status and emits an event:

```rust
match &operation_result {
    Ok(result) => {
        results.insert(id.clone(), serde_json::to_string(result)?);
        OperationStatus::Completed
    }
    Err(e) => OperationStatus::Failed(e.to_string()),
};
// ...
event_hub.send_event(Event {
    origin: Origin::LongOperation(LongOperationEvent::Failed),
    data: Some(json!({"id": id, "error": error_string}).to_string()),
    ..
});
```

On failure, no result is stored. The controller's `get_*_result()` returns `Ok(None)`, which is **ambiguous**: it also returns `Ok(None)` when the operation is still running. Callers must check the operation status separately via `get_operation_status()` to distinguish "not finished yet" from "failed." The error message is available through the status enum and through the event's JSON payload.

### Summary

| Scenario | C++/Qt | Rust |
|----------|--------|------|
| Repository call fails mid-transaction | `catch(...)` calls `rollback()` + `SignalBuffer::discard()` | `?` returns `Err`; UoW dropped; `EventBuffer` freed |
| `beginTransaction()` fails | Throws `std::runtime_error`, caught by same `catch(...)` | `?` returns `Err`; no transaction was opened |
| `commit()` fails | Throws `std::runtime_error`; `UnitOfWorkBase` already discards the signal buffer | `?` returns `Err`; transaction dropped, auto-savepoint restored |
| `execute()` fails at controller level | Command dropped from undo stack; default result returned to UI; `errorOccurred` signal emitted via registry → `ServiceLocator` | Use case dropped; never added to undo stack; `Err` propagated |
| Undo fails | Command moved back from redo to undo stack (retryable) | Command re-pushed to undo stack (retryable) |
| Redo fails | Command dropped from undo stack | Command re-pushed to redo stack (retryable) |
| Composite undo partially fails | N/A (composites are Rust-only) | Short-circuits; already-undone sub-commands stay committed; composite dropped |
| Long operation fails | `operationFailed` signal emitted; no result stored | `Failed` status set; `Failed` event emitted; `get_*_result()` returns `None` |

## Where the Code Lives

### C++/Qt (file count varies by manifest)

```
src/
├── direct_access/
│   └── calendar/                    # per-entity package
│       ├── calendar_controller.cpp  # entry point for UI
│       ├── calendar_unit_of_work.h  # UoW with transaction + signal buffer
│       ├── dtos.h                   # CalendarDto, CreateCalendarDto
│       ├── models/                  # reactive QML list models
│       └── use_cases/               # CRUD use cases with undo/redo
├── calendar_management/             # feature package
│   ├── calendar_management_controller.cpp
│   ├── calendar_management_dtos.h
│   ├── units_of_work/               # feature-specific UoWs
│   └── use_cases/                   # feature use cases
└── common/
    ├── direct_access/               # repositories, tables, events per entity
    │   ├── event_registry.h         # centralizes all entity event objects
    │   └── calendar/
    │       ├── calendar_repository.cpp  # CRUD + event emission via SignalBuffer
    │       ├── calendar_events.h        # Qt signals for created/updated/removed
    │       └── calendar_table.cpp       # SQLite operations + cache
    ├── features/
    │   ├── feature_event_registry.h     # centralizes feature event objects
    │   └── calendar_management_events.h # Qt signals per feature use case
    ├── undo_redo/                    # command pattern + async execution
    ├── unit_of_work/                 # base classes, CRTP helpers
    ├── long_operation/               # threaded execution with progress
    ├── signal_buffer.h               # deferred event delivery
    └── database/                     # DbContext, junction tables, caches
```

### Rust (file count varies by manifest)

```
src/
├── direct_access/src/
│   └── calendar/                    # per-entity package
│       ├── calendar_controller.rs   # free functions, entry point
│       ├── dtos.rs                  # CalendarDto, CreateCalendarDto
│       ├── units_of_work.rs         # UoW + UoWRO with HashMap store transactions
│       └── use_cases/               # CRUD use cases with UndoRedoCommand trait
├── calendar_management/src/
│   ├── calendar_management_controller.rs  # feature controller
│   ├── dtos.rs
│   ├── units_of_work/               # feature-specific UoWs
│   └── use_cases/                   # feature use cases
├── common/src/
│   ├── direct_access/               # repositories, tables per entity
│   │   ├── calendar/
│   │   │   ├── calendar_repository.rs  # CRUD + event emission via EventBuffer
│   │   │   └── calendar_table.rs       # HashMap store operations
│   │   └── repository_factory.rs       # creates repositories within transactions
│   ├── event.rs                     # EventHub, Event, Origin enums (all events)
│   ├── undo_redo.rs                 # UndoRedoManager, multi-stack, composites
│   ├── long_operation.rs            # threaded execution with progress
│   └── database/                    # DbContext, transactions
├── macros/src/                      # procedural macros for UoW boilerplate
├── frontend/src/                    # entry point for UI or CLI to interact with entities and features
│   ├── src/
        ├── event_hub_client.rs         # event hub client
        ├── app_context.rs              # holds the instances needed by the backend
        ├── commands.rs
        └── commands/                   # convenient wrappers for controller APIs
            ├── undo_redo_commands.rs
            ├── calendar_commands.rs
            ├── calendar_management_commands.rs
            ├── event_commands.rs
            └── root_commands.rs

```

## Summary of Differences

| Aspect | C++/Qt | Rust |
|--------|--------|------|
| Execution model | Async (QCoro coroutines) | Synchronous |
| Command execution | Undo/redo system executes the command | Use case executes, then pushed to stack |
| Event deferral | SignalBuffer (explicit buffer/flush/discard) | EventBuffer (explicit buffer/flush/discard) |
| Event registries | Separate per entity + separate per feature | Single EventHub with Origin enum |
| Long operations | QtConcurrent::run | std::thread::spawn |
| Database | SQLite (WAL mode) | in-memory HashMap store |
| Cascade snapshots | Yes (table-level snapshot/restore) | Yes (table-level snapshot/restore) |
| UoW boilerplate | CRTP templates (entities), macros (feature use cases) | Procedural macros (`#[macros::uow_action]`) |
| Read-only queries | Through undo/redo system (serialization) | Direct call with read-only UoW |