Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PyPI - Version license quality GitHub commit activity

Qleany

Define your entities in YAML. Get a complete, tested architecture in C++20/Qt6 or Rust — controllers, repositories, undo/redo, reactive models, and ready-to-compile UIs.

No framework. No runtime. No Qleany dependencies in your code.

The generated code is yours — plain C++ classes and Rust structs using standard libraries (Qt, QCoro, redb). Modify it, extend it, delete Qleany afterward. You’re not adopting a framework that will haunt your codebase for years or burn you when the maintainer moves on.

Qt provides excellent widgets and signals, but little guidance on organizing a 30,000-line application. Rust’s GUI ecosystem is growing fast, but there’s nothing to help you structure what sits behind the UI. Qleany fills that gap. Write a YAML manifest describing your entities, relationships, and features. Qleany generates the rest: the database layer, the repository infrastructure, the event system, the controller wiring, and — if you need it — a multi-stack undo/redo system with cascade snapshot/restore for entity trees. For C++/Qt, it also generates reactive QML models that update themselves, and JavaScript mock controllers so your UI developer can work without waiting for the backend.

For a 13-entity project, that’s roughly 600 files in C++/Qt or 300 in Rust, all compiling, all internally consistent, with a generated test suite that validates the infrastructure before you write a single line of business logic. The generated code is deliberately straightforward — readable and modifiable by a developer with a few years of experience, not a showcase of advanced language features.

Qleany follows Package by Feature (Vertical Slice Architecture) principles. Define your entities and features once, generate consistent architecture across Rust and C++/Qt with baked-in (empty) UIs. Qleany’s own Slint-based tool is built using the same patterns it generates.

Key Features

  • Complete CRUD infrastructure — Controllers, DTOs, use cases, repositories per entity
  • Undo/redo system (optional) — Command-based with multi-stack scoping, composite grouping, and failure strategies; async execution with QCoro coroutines in C++/Qt, synchronous in Rust; cascade snapshot/restore for entity trees
  • GUI skeleton generation — Ready-to-compile frontend code for QtQuick, QtWidgets, Slint, or CLI
  • Reactive QML models — Auto-updating list models and single-entity wrappers with event-driven refresh (C++/Qt)
  • QML mocks — JavaScript stubs that simulate async behavior, enabling UI development without a backend (C++/Qt)
  • Relationship management — Uniform junction tables with ordering, two-layer caching, bidirectional navigation, and cascade deletion
  • Event system — Thread-safe, decoupled communication between features
  • Generated test suite — Junction table operations, undo/redo behavior, and async integration tests

Documentation

DocumentPurpose
Quick Start - RustStep-by-step tutorial building a complete application
Quick Start - C++/QtStep-by-step tutorial building a complete application
Manifest ReferenceEntity options, field types, relationships, features and use cases
Design PhilosophyClean Architecture background, package by feature, Rust module structure
Regeneration WorkflowHow file generation works, what gets overwritten, files that must stay in sync
Undo-Redo ArchitectureEntity tree structure, undoable vs non-undoable, configuration patterns
QML IntegrationReactive models, mocks, and event system for C++/Qt
Generated Infrastructure - C++/QtDatabase layer, repositories, and file organization details
Generated Infrastructure - RustDatabase layer, repositories, and file organization details
TroubleshootingCommon issues and how to fix them

New to Qleany? Start with the Quick Start Guide - C++/Qt or Quick Start Guide - Rust. Then return here for reference.

Screenshot


Is Qleany the Right Fit?

When Qleany Makes Sense

Data-centric applications that will grow in complexity over time. Think document editors, project management tools, creative applications, or anything where users manipulate structured data and expect undo/redo to work reliably. This applies equally to desktop and mobile — a note-taking app on Plasma Mobile has the same architectural needs as one on desktop Linux.

Complex CLI tools in Rust — tools like git that manage structured data, have multiple subcommands, and need consistent internal architecture. Qleany itself is built this way: type qleany -h to see a CLI interface backed by the same architecture that powers its Slint GUI.

Applications targeting multiple platforms — if you’re building for desktop Linux and want to support Plasma Mobile or Ubuntu Touch with the same codebase, Qleany’s generated backend works identically across all of them. Write your business logic once, swap UI frontends as needed.

Applications needing multiple Qt frontends — if you need QtQuick, QtWidgets (or any combination of them simultaneously), Qleany generates a ready-to-compile backend architecture that any of these frontends can consume. The generated controllers, repositories, and event system work identically regardless of which UI toolkit you choose.

Solo developers or small teams without established architectural patterns. Qt provides excellent widgets and signals, but little guidance on organizing a 30,000-line application (or I couldn’t find it). Qleany gives you that structure immediately, with patterns validated through real-world use in Skribisto.

Projects that will grow incrementally — the manifest-driven approach means you can define a new entity, regenerate the architecture, and immediately have a working controller, repository, DTOs, and use cases. The consistency this brings across your codebase is hard to achieve manually.

When to Reconsider

For simple utilities or single-purpose tools, Qleany introduces more infrastructure than you need. If your application doesn’t have complex entity relationships, doesn’t need undo/redo, and won’t grow significantly, a hand-written architecture may serve you better.

If you’re working with a team that already has established patterns, introducing Qleany means everyone needs to learn its conventions. The generated code is readable and follows clear patterns, but it represents a specific way of doing things. Discuss with your team before adopting it. Do not antagonize existing workflows. A more professional approach may be to present Qleany’s patterns with some open-minded senior devs of your team. Even if they don’t want to use Qleany - which is fairly expected - they may appreciate some of its ideas and adapt them to their existing architecture. They may even want to use Qleany for prototyping or side projects, or scaffold new subsystems of an existing project without disrupting the main architecture.

Qleany targets native applications. If you’re building for the web, using Electron, this isn’t the right tool. Similarly, if you need high-throughput server-side processing, the patterns here are optimized for user interaction, not request-per-second performance.

Special Considerations

If you are targeting Android/iOS with Flutter or React Native, the Rust as a backend option can be an interesting choice, but the C++/Qt generation is not. Any Rust backend can use UniFFI or other means to call Rust from nearly any frontend accepting FFI (SwiftUI, Kotlin, etc…)

You can also have a Rust backend and a C++/Qt frontend in the same codebase, using cxx-qt as a bridge.

The Practical Test

If your project matches the profile, start by generating the architecture for a small subset of your entities and spend time reading through the generated code. Understand how the controllers wire to use cases, how the event system propagates changes, how the undo commands work. This investment of a few hours will tell you whether the patterns feel natural to your way of thinking.

The “generate and disappear” philosophy means you’re not locked in. If you decide halfway through that you’d prefer a different approach, the generated code is yours to modify or replace.


Why Qleany

I wrote Skribisto, a novel-writing application in Qt. Four times. In different languages. Each time, I hit the same wall: spaghetti code and structural dead-ends that made adding features painful and eventually impossible without rewriting half the codebase.

After the third rewrite, I studied architecture patterns seriously. Clean Architecture (Robert C. Martin) clicked — the separation of concerns, the dependency rules, the testability. But implementing it by hand meant writing the same boilerplate over and over: repositories, DTOs, use cases, controllers. So I wrote templates. The templates grew into a generator. The generator needed a manifest file.

Qleany v0 was Python/Jinja2 generating C++/Qt code following pure Clean Architecture. It worked, but the tradeoffs were hard to miss: a 17-entity project produced 1700+ files across 500 folders. Some of my early design choices were dubious in hindsight.

Qleany v1 is a ground-up rewrite in Rust, aiming to fix those problems while adopting a more robust and easier-to-maintain language. Less sophisticated, more pragmatic, architecture. It adopts Package by Feature (a.k.a. Vertical Slice Architecture) instead of strict layer separation — same Clean Architecture principles, but organized by what the code does rather than what layer it belongs to. The same manifest now generates both C++/Qt and Rust code.

This is the tool I needed when I started Skribisto. If it saves someone else from their fourth rewrite, it’s done its job.


Target Platforms

LanguageStandardinternal databaseFrontend Options
C++C++20 / Qt6SQLiteQtQuick, QtWidgets
RustRust 2024redbCLI, Slint

Supported deployment targets for C++/Qt:

  • Desktop Linux (KDE Plasma, GNOME, etc.)
  • Plasma Mobile
  • Ubuntu Touch
  • Windows, macOS (Qt’s cross-platform support)

Supported deployment targets for Rust:

  • All the usual Rust targets (Linux, Windows, macOS, etc.)

The generated backend is platform-agnostic. Your business logic, repositories, and controllers work identically whether you’re building a desktop app, a mobile app, or both from the same codebase. Only the UI layer differs.

Also, the internal database choice (SQLite for C++/Qt, redb for Rust) is abstracted behind repositories. You can swap out the database implementation if needed, though SQLite and redb are solid choices for most applications.

Rust frontend examples (working references):

I’m no web developer, and Tauri/React is not my forte. But if you want to build a web-based frontend with Rust backend generated by Qleany, this is a starting point.


Where to Get Qleany

SourceStatus
GitHub ReleasesSee here
Cargocargo install --git https://github.com/jacquetc/qleany qleany or cargo binstall qleany
PyPI (pip) with pipxpipx install qleany from Pypi

Or build from source (see below).


License

Qleany (the generator) is licensed under MPL-2.0. See the LICENSE file for details. It is compatible with both open source and proprietary projects.

Generated code: This license does not cover the code generated by Qleany. You are free to use, modify, and distribute generated code under any license of your choice, including proprietary licenses.

For more details, see this fine summary


Building and Running

Prerequisites

Building Qleany

git clone https://github.com/jacquetc/qleany
cd qleany
cargo build --release

Running the UI

cargo run --release

The Slint-based UI provides:

  • Form-based manifest editing
  • Entity and relationship management
  • Selective file generation
  • Code preview before writing

For more details, see the Quick Start Guide - C++/Qt or Quick Start Guide - Rust.

CLI Usage


# Show help
qleany -h


# Show an option help
qleany generate -h


# show the list of available documentation
qleany doc -h

# show all documentation
qleany doc

# new qleany.yaml manifest
qleany new --language cpp-qt (or rust)

# Generate all files
qleany generate

# Dry run (list files that would be generated without writing)
qleany generate --dry-run

# Dry run (list files that would be generated without writing)
qleany generate --dry-run entity MyEntity

# Generate to temp folder (recommended)
qleany generate --temp

# Generate specific feature
qleany generate feature my_feature_name

# List files that would be generated
qleany list

# List features that would be generated
qleany list features

Reference Implementation

Skribisto (develop branch) is a novel-writing application built with Qleany-generated C++/Qt code. It demonstrates:

  • Full entity hierarchy (Root → Work → Binder → BinderItem → Content)
  • Complex relationships (ordered children, many-to-many tags)
  • Feature orchestration (LoadWork, SaveWork with file format transformation)
  • Reactive QML UI with auto-updating models
  • Undo/redo across structural and content operations

Skribisto serves as both proof-of-concept and template source for C++/Qt generation.


Migration from v0

Qleany v0 (Python/Jinja2) generated pure Clean Architecture with strict layer separation. A 17-entity project produced 1700+ files across 500 folders. Yes, version “zero” is the first version, the prototype.

v1 generates Package by Feature with pragmatic organization. The same project produces ~600 files across ~80 folders with better discoverability. Its manifest version begins with version 2.

Breaking changes:

  • Manifest format changed (schema version 2)
  • Output structure reorganized by feature
  • Reactive models are new (list models, singles)

Bottom line: from v0 to v1, there is no automated migration path. You must regenerate from your manifest and manually port any custom code.

Starting from the newer version 2 of the manifest (i.e., Qleany v1), the new architecture will allow a smoother transition to future versions.


Contributing

Qleany is developed alongside Skribisto. The architecture is stable, but templates are being extracted and refined.

To contribute:

  1. Open an issue to discuss changes
  2. Ensure changes work for both Rust and C++/Qt
  3. Remember to sign off your commits (commit -s)

Please read the CONTRIBUTING.md file.

Support

GitHub Issues is the only support channel: github.com/jacquetc/qleany/issues

Qleany is a project licensed under MPL-2.0. It is actively used in Skribisto’s development, among other projects from FernTech, so improvements flow from real-world needs. Bug reports and contributions are welcome.

FernTech offers professional support for Qleany.

AI use

Qleany is not an AI tool. It is a human-driven tool using templates and smart (I can hope) algorithms.

In this era where too much code comes from AI, and too much “slop” code, I feel that I must be honest with my use of these semi-smart tools. I am an IT professional for 14 years (started in 2011). I was Linux sysadmin, DevOps engineer, and now a senior C++ and Rust developer/tech lead/architect. I learned my trade before all this AI stuff. For me, LLMs are capricious smart tools. My take: never trust a LLM, always check the answers because they tend to trick you, LLMs never learn from their mistakes, unlike a human. It’s a tool, not a crutch. [Ranting mode: off]

In Qleany, AI was used in only three cases:

  • basic auto-completion, thanks to the LLM integrated into JetBrains IDEs (especially in very repetitive patterns), less “magical” than GitHub Copilot, but still helpful.
  • English sentences in the documentation were smoothed with the AI, nothing more. And it helped to create tables with all these asterisks. I wrote this documentation.
  • The AI added some comments and inline documentation, especially in the C++ undo redo system. It was fun.

That’s it. I honestly feel that Qleany is the work of a human being (me), not a machine.

About

Qleany is developed and maintained by FernTech.

Copyright (c) 2025-2026 FernTech Licensed under MPL-2.0

Qleany Quick Start - Rust

This guide walks you through creating a complete desktop application for a car dealership using Qleany. By the end, you’ll have generated architecture with entities, repositories, controllers, and undo/redo infrastructure.

For C++ / Qt, see Qleany Quick Start - C++/Qt. The differences are minor.

The qleany.yaml of this example is available here.


Step 1: Think About Your Domain

Before touching any tool, grab paper or open a diagramming tool. This is the most important step.

Ask yourself:

  • What are the core “things” in my business? These become entities.
  • What actions do users perform? These become use cases.
  • Which use cases belong together? These become features.

Example: CarLot — A Car Dealership App

Entities (the nouns):

EntityPurposeKey Fields
EntityBaseBase class for all entitiesid, created_at, updated_at
RootApplication entry point, owns everythingcars, customers, sales
CarVehicle in inventorymake, model, year, price, status
CustomerPotential or actual buyername, email, phone
SaleCompleted transactionsale_date, final_price, car, customer

Relationships:

  • Root owns many Cars (inventory)
  • Root owns many Customers (contacts)
  • Root owns many Sales (history)
  • Sale references one Car (what was sold)
  • Sale references one Customer (who bought it)

Features and Use Cases (the verbs):

FeatureUse CaseWhat it does
inventory_managementimport_inventoryParse CSV file, populate Car entities
inventory_managementexport_inventoryGenerate CSV from current inventory

Draw It First

Sketch your entities and relationships before using Qleany. Use paper, whiteboard, or Mermaid.

Deeper explanations about relationships are available in the Manifest Reference.

erDiagram
    EntityBase {
        EntityId id
        datetime created_at
        datetime updated_at
    }

    Root {
        EntityId id
        datetime created_at
        datetime updated_at
        # relationships:
        Vec<EntityId> cars
        Vec<EntityId> customers
        Vec<EntityId> sales
    }
    
    Car {
        EntityId id
        datetime created_at
        datetime updated_at
        string make
        string model
        int year
        float price
        enum status
    }
    
    Customer {
        EntityId id
        datetime created_at
        datetime updated_at
        string name
        string email
        string phone
    }
    
    Sale {
        EntityId id
        datetime created_at
        datetime updated_at
        datetime sale_date
        float final_price
        int car_id
        int customer_id
        # relationships:
        EntityId car
        EntityId customer
    }

    EntityBase ||--o{ Root : "inherits"
    EntityBase ||--o{ Car : "inherits"
    EntityBase ||--o{ Customer : "inherits"
    EntityBase ||--o{ Sale : "inherits"
    Root ||--o{ Car : "owns (strong)"
    Root ||--o{ Customer : "owns (strong)"
    Root ||--o{ Sale : "owns (strong)"
    Sale }o--|| Car : "optionally references"  # Many-to-One (a sale may exist without a car, e.g., if the car was deleted)
    Sale }o--|| Customer : "optionally references" # Many-to-One 

Why draw first? Changing a diagram is free. Changing generated code is work. Get the model right before generating.

EntityBase is a common pattern: it provides shared fields like id, created_at, and updated_at, like an inheritance. Other entities can explicitly inherit from it. This is not an entity. It will never be generated. All your entities can inherit from it to avoid repetition.

Note: You can note the relationships on the diagram too. Qleany supports various relationship types (one-to-one, one-to-many, many-to-one, many-to-many) and cascade delete (strong relationships). Defining these upfront helps you configure them correctly in the manifest. Unlike typical ER diagrams, the relationships appear as fields. Forget the notion of foreign keys here. Qleany’s relationships are directional and can be configured with additional options (e.g., ordered vs unordered, strong vs weak, optional or not (only for some relationship types)). Plan these carefully to ensure the generated code matches your intended data model.

WRONG: I only need a few entities without any “owner” relationships. I can just create them in Qleany and skip the Root entity.

RIGHT: I want a clear ownership structure. Root owns all Cars, Customers, and Sales. This makes it easy to manage the lifecycle of entities. It avoids orphan entities and simplifies the generated code. Even if Root has few fields, it provides a clear parent-child structure. Think like a tree: Root is the trunk, Cars/Customers/Sales are branches. This is a common pattern in Qleany projects.


Step 2: Create a New Manifest

Launch Qleany. You’ll land on the Home tab.

  1. Click New Manifest
  2. Choose where to save qleany.yaml (your project root)

Qleany creates a minimal manifest with:

  • EntityBase (provides id, created_at, updated_at)
  • Empty Root entity inheriting from EntityBase

Step 3: Configure Project Settings

Click Project in the sidebar.

Fill in the form:

FieldValue
LanguageRust
Application NameCarLot
Organisation NameMyCompany
Organisation Domaincom.mycompany
Prefix Pathcrates

Organisation Domain is used for some installed file names, like the icon name.

Changes save. The header shows “Save Manifest” when there are unsaved changes.


Step 4: Define Entities

Click Entities in the sidebar. You’ll see a three-column layout.

4.1 Create the Car Entity

  1. Click the + button next to “Entities”
  2. A new entity appears — click it to select
  3. In the details panel:
    • Name: Car
    • Inherits from: EntityBase

Now add fields. In the “Fields” section:

  1. Click + to add a field
  2. Select the new field, then configure:
NameTypeNotes
makeString
modelString
yearInteger
priceFloat
statusEnumEnum Name: CarStatus, Values: Available, Reserved, Sold (one per line)

4.2 Create the Customer Entity

  1. Click + next to “Entities”
  2. Name: Customer
  3. Inherits from: EntityBase
  4. Add fields:
NameType
nameString
emailString
phoneString

4.3 Create the Sale Entity

  1. Click + next to “Entities”
  2. Name: Sale
  3. Inherits from: EntityBase
  4. Add fields:
NameTypeConfiguration
sale_dateDateTime
final_priceFloat
carEntityReferenced Entity: Car, Relationship: many_to_one
customerEntityReferenced Entity: Customer, Relationship: many_to_one

4.4 Configure Root Relationships

Select the Root entity. Add relationship fields:

NameTypeConfiguration
carsEntityReferenced Entity: Car, Relationship: ordered_one_to_many, Strong: ✓
customersEntityReferenced Entity: Customer, Relationship: ordered_one_to_many, Strong: ✓
salesEntityReferenced Entity: Sale, Relationship: ordered_one_to_many, Strong: ✓

Key concepts:

  • Strong relationship: Deleting Root cascades to delete all Cars, Customers, Sales

Step 5: Define Features and Use Cases

Click Features in the sidebar. You’ll see a four-column layout.

5.1 Create the Feature

  1. Click + next to “Features”
  2. Select it and set Name: inventory_management

5.2 Create the Import Use Case

  1. Click + next to “Use Cases”
  2. Configure:
FieldValue
Nameimport_inventory
Undoable(file imports typically aren’t undoable)
Read Only(it will update the internal database)
Long Operation(parsing files can take time)
  1. Switch to the DTO In tab:

    • Enable the checkbox
    • Name: ImportInventoryDto
    • Add field: file_path (String)
  2. Switch to the DTO Out tab:

    • Enable the checkbox
    • Name: ImportResultDto
    • Add fields: imported_count (Integer), error_messages (String, List: ✓)
  3. Switch to the Entities tab:

    • Check: Root, Car

5.3 Create the Export Use Case

  1. Click + next to “Use Cases”
  2. Configure:
FieldValue
Nameexport_inventory
Undoable
Read Only(just reading internal data)
Long Operation
  1. DTO In:

    • Name: ExportInventoryDto
    • Field: output_path (String)
  2. DTO Out:

    • Name: ExportResultDto
    • Field: exported_count (Integer)
  3. Entities: Check Root, Car

5.4 Choose your UI

For Rust, choose between CLI, Slint UI, or both. These options scaffold basic UI or CLI code that interacts with the generated controllers. You can skip this and build your own UI later if you prefer.

For Slint, Qleany generates a basic Slint UI, event system integration and generates command files to bind the UI to the generated controllers.

CLI uses clap for you to build a command line interface.

5.5 Save the Manifest

Click Save Manifest in the header (or Ctrl+S).

5.6 Take a break, drink a coffee, sleep a bit

I mean it. A fresher mind sees things more clearly. You already saved a lot of time by using Qleany instead of writing all the boilerplate yourself. Don’t rush the design phase, it’s where you get the most value from Qleany.

Designing your domain and use cases is the most important part. The generated code is a complete architecture, not mere scaffolding. If the model is wrong, the code won’t help much. Take your time to get it right before generating.

Yes, you can change the manifest and regenerate later. But it’s better to get a solid design upfront. The more you change the model after generating, the more work you create for yourself. It’s not a problem to evolve your design, but try to avoid major changes that require rewriting large parts of the generated code.


Step 6: Save and Generate

Commit to Git

Before generating, commit your current state to Git. This isn’t optional advice — it’s how Qleany is meant to be used. If you accidentally overwrite files you’ve modified, you can restore them.

git add .
git commit -m "Before Qleany generation"

Generate Code

  1. Click Generate in the sidebar
  2. Review the groups and files
  3. (Optional) Check in temp/ to generate to a temporary folder first
  4. Click a file to preview the generated code
  5. Click Generate (N) where N is the number of selected files

The progress modal shows generation status. Files are written to your project.

The files are formatted with cargo fmt.


Step 7: What You Get

After a generation, your project contains:

Cargo.toml
crates/
├── cli/
│   ├── src/
│   │   ├── main.rs    
│   └── Cargo.toml
├── common/
│   ├── src/
│   │   ├── entities.rs             # Car, Customer, Sale structs
│   │   ├── database.rs
│   │   ├── database/
│   │   │   ├── db_context.rs
│   │   │   ├── db_helpers.rs
│   │   │   └── transactions.rs
│   │   ├── direct_access.rs
│   │   ├── direct_access/         # Holds the repository and table implementations for each entity
│   │   │   ├── car.rs
│   │   │   ├── car/
│   │   │   │   ├── car_repository.rs
│   │   │   │   └── car_table.rs
│   │   │   ├── customer.rs
│   │   │   ├── customer/
│   │   │   │   ├── customer_repository.rs
│   │   │   │   └── customer_table.rs
│   │   │   ├── sale.rs
│   │   │   ├── sale/
│   │   │   │   ├── sale_repository.rs
│   │   │   │   └── sale_table.rs
│   │   │   ├── root.rs
│   │   │   ├── root/
│   │   │   │   ├── root_repository.rs
│   │   │   │   └── root_table.rs
│   │   │   ├── repository_factory.rs
│   │   │   └── setup.rs
│   │   ├── event.rs             # event system for reactive updates
│   │   ├── lib.rs
│   │   ├── long_operation.rs    # infrastructure for long operations
│   │   ├── types.rs         
│   │   └── undo_redo.rs        # undo/redo infrastructure
│   └── Cargo.toml
├── direct_access/                   # a direct access point for UI or CLI to interact with entities
│   ├── src/
│   │   ├── car.rs
│   │   ├── car/
│   │   │   ├── car_controller.rs   # Exposes CRUD operations to UI or CLI
│   │   │   ├── dtos.rs
│   │   │   ├── units_of_work.rs
│   │   │   ├── use_cases.rs
│   │   │   └── use_cases/          # The logic here is auto-generated
│   │   │       ├── create_car_uc.rs
│   │   │       ├── get_car_uc.rs
│   │   │       ├── update_car_uc.rs
│   │   │       ├── remove_car_uc.rs
│   │   │       └── ...
│   │   ├── customer.rs
│   │   ├── customer/
│   │   │   └── ...
│   │   ├── sale.rs
│   │   ├── sale/
│   │   │   └── ...
│   │   ├── root.rs
│   │   ├── root/
│   │   │   └── ...
│   │   └── lib.rs
│   └── Cargo.toml
└── inventory_management/
    ├── src/
    │   ├── inventory_management_controller.rs     # Exposes operations to UI or CLI
    │   ├── dtos.rs
    │   ├── units_of_work.rs
    │   ├── units_of_work/          # adapt the macros here 
    │   │   └── ...
    │   ├── use_cases.rs
    │   ├── use_cases/              # You implement the logic here
    │   │   └── ...
    │   └── lib.rs
    └── Cargo.toml

What’s generated:

  • Complete CRUD for all entities (create, get, update, remove, …)
  • Controllers exposing operations
  • DTOs for data transfer
  • Repository pattern for database access
  • Undo/redo infrastructure for undoable operations
  • Tests suites for the database and undo redo infrastructure
  • Event system for reactive updates
  • Basic CLI (if selected during project setup)
  • Basic empty Slint UI (if selected during project setup)

What you implement:

  • Your custom use case logic (import_inventory, export_inventory)
  • Your UI or CLI on top of the controllers or their adapters.

Step 8: Run the Generated Code

Let’s assume that you have Rust installed.

In a terminal,

cargo run

Next Steps

  1. Run the generated code — it compiles and provides working CRUD
  2. Implement your custom use cases (import_inventory, export_inventory)
  3. Build your UI on top of the controllers
  4. Add more features as your application grows

The generated code is yours. Modify it, extend it, or regenerate when you add new entities. Qleany gets out of your way.

Tips

Understanding the Internal Database

Entities are stored in an internal database (redb for Rust). This database is internal, users and UI devs don’t interact with it directly.

Typical pattern:

  1. User opens a file (e.g., .carlot project file)
  2. Your load_project use case parses the file and populates entities
  3. User works — all changes go to the internal database
  4. User saves — your save_project use case serializes entities back to file

The internal database is ephemeral. It enables fast operations, undo/redo. The user’s file is the permanent storage.

Undo/Redo

Every generated CRUD operation supports undo/redo automatically. You don’t have to display undo/redo controls in your UI if you don’t want to, but the infrastructure is there when you need it.

If you mark a use case as Undoable, Qleany generates the command pattern scaffolding. You fill in what “undo” means for your specific operation.

For more information, see Undo-Redo Architecture.

Relationships

RelationshipUse When
one_to_oneExclusive 1:1 (User → Profile)
many_to_oneChild references parent (Sale → Car)
one_to_manyParent owns unordered children
ordered_one_to_manyParent owns ordered children (chapters in a book)
many_to_manyShared references (Items ↔ Tags)

Strong means cascade delete — deleting the parent deletes children.

For more details, see Manifest Reference.

Regenerating

Made a mistake? The manifest is just YAML. You can:

  • Edit it directly in a text editor or from the GUI tool
  • Delete entities/features in the UI and recreate them
  • Generate to a temp folder, review, then regenerate to the real location

For more details, see Regeneration Workflow.


The generated code is yours. Modify it, extend it, or regenerate when you add new entities. Qleany gets out of your way.


Further Reading

Qleany Quick Start - C++/Qt

This guide walks you through creating a complete desktop application for a car dealership using Qleany. By the end, you’ll have generated architecture with entities, repositories, controllers, and undo/redo infrastructure.

For Rust, see Qleany Quick Start - Rust. The differences are minor.

The qleany.yaml of this example is available here.


Step 1: Think About Your Domain

Before touching any tool, grab paper or open a diagramming tool. This is the most important step.

Ask yourself:

  • What are the core “things” in my business? These become entities.
  • What actions do users perform? These become use cases.
  • Which use cases belong together? These become features.

Example: CarLot — A Car Dealership App

Entities (the nouns):

EntityPurposeKey Fields
EntityBaseBase class for all entitiesid, created_at, updated_at
RootApplication entry point, owns everythingcars, customers, sales
CarVehicle in inventorymake, model, year, price, status
CustomerPotential or actual buyername, email, phone
SaleCompleted transactionsale_date, final_price, car, customer

Relationships:

  • Root owns many Cars (inventory)
  • Root owns many Customers (contacts)
  • Root owns many Sales (history)
  • Sale references one Car (what was sold)
  • Sale references one Customer (who bought it)

Features and Use Cases (the verbs):

FeatureUse CaseWhat it does
inventory_managementimport_inventoryParse CSV file, populate Car entities
inventory_managementexport_inventoryGenerate CSV from current inventory

Draw It First

Sketch your entities and relationships before using Qleany. Use paper, whiteboard, or Mermaid.

Deeper explanations about relationships are available in the Manifest Reference.

erDiagram
    EntityBase {
        EntityId id
        datetime created_at
        datetime updated_at
    }

    Root {
        EntityId id
        datetime created_at
        datetime updated_at
        # relationships:
        QList<EntityId> cars
        QList<EntityId> customers
        QList<EntityId> sales
    }
    
    Car {
        EntityId id
        datetime created_at
        datetime updated_at
        string make
        string model
        int year
        float price
        enum status
    }
    
    Customer {
        EntityId id
        datetime created_at
        datetime updated_at
        string name
        string email
        string phone
    }
    
    Sale {
        EntityId id
        datetime created_at
        datetime updated_at
        datetime sale_date
        float final_price
        int car_id
        int customer_id
        # relationships:
        EntityId car
        EntityId customer
    }

    EntityBase ||--o{ Root : "inherits"
    EntityBase ||--o{ Car : "inherits"
    EntityBase ||--o{ Customer : "inherits"
    EntityBase ||--o{ Sale : "inherits"
    Root ||--o{ Car : "owns (strong)"
    Root ||--o{ Customer : "owns (strong)"
    Root ||--o{ Sale : "owns (strong)"
    Sale }o--|| Car : "optionally references"  # Many-to-One (a sale may exist without a car, e.g., if the car was deleted)
    Sale }o--|| Customer : "optionally references" # Many-to-One 

Why draw first? Changing a diagram is free. Changing generated code is work. Get the model right before generating.

EntityBase is a common pattern: it provides shared fields like id, created_at, and updated_at, like an inheritance. Other entities can explicitly inherit from it. This is not an entity. It will never be generated. All your entities can inherit from it to avoid repetition.

Note: You can note the relationships on the diagram too. Qleany supports various relationship types (one-to-one, one-to-many, many-to-one, many-to-many) and cascade delete (strong relationships). Defining these upfront helps you configure them correctly in the manifest. Unlike typical ER diagrams, the relationships appear as fields. Forget the notion of foreign keys here. Qleany’s relationships are directional and can be configured with additional options (e.g., ordered vs unordered, strong vs weak, optional or not (only for some relationship types)). Plan these carefully to ensure the generated code matches your intended data model.

WRONG: I only need a few entities without any “owner” relationships. I can just create them in Qleany and skip the Root entity.

RIGHT: I want a clear ownership structure. Root owns all Cars, Customers, and Sales. This makes it easy to manage the lifecycle of entities. It avoids orphan entities and simplifies the generated code. Even if Root has few fields, it provides a clear parent-child structure. Think like a tree: Root is the trunk, Cars/Customers/Sales are branches. This is a common pattern in Qleany projects.


Step 2: Create a New Manifest

Launch Qleany. You’ll land on the Home tab.

  1. Click New Manifest
  2. Choose where to save qleany.yaml (your project root)

Qleany creates a minimal manifest with:

  • EntityBase (provides id, created_at, updated_at)
  • Empty Root entity inheriting from EntityBase

Step 3: Configure Project Settings

Click Project in the sidebar.

Fill in the form:

FieldValue
LanguageC++/Qt
Application NameCarLot
Organisation NameMyCompany
Organisation Domaincom.mycompany
Prefix Pathsrc

Organisation Domain is used for some installed file names, like the icon name.

Changes save. The header shows “Save Manifest” when there are unsaved changes.


Step 4: Define Entities

Click Entities in the sidebar. You’ll see a three-column layout.

4.1 Create the Car Entity

  1. Click the + button next to “Entities”
  2. A new entity appears — click it to select
  3. In the details panel:
    • Name: Car
    • Inherits from: EntityBase

You can also enable the Single Model checkbox to generate a helper class for the entity and its QML wrapper.

Now add fields. In the “Fields” section:

  1. Click + to add a field
  2. Select the new field, then configure:
NameTypeNotes
makeString
modelString
yearInteger
priceFloat
statusEnumEnum Name: CarStatus, Values: Available, Reserved, Sold (one per line)

4.2 Create the Customer Entity

  1. Click + next to “Entities”
  2. Name: Customer
  3. Inherits from: EntityBase
  4. Add fields:
NameType
nameString
emailString
phoneString

4.3 Create the Sale Entity

  1. Click + next to “Entities”
  2. Name: Sale
  3. Inherits from: EntityBase
  4. Add fields:
NameTypeConfiguration
sale_dateDateTime
final_priceFloat
carEntityReferenced Entity: Car, Relationship: many_to_one
customerEntityReferenced Entity: Customer, Relationship: many_to_one

4.4 Configure Root Relationships

Select the Root entity. Add relationship fields:

NameTypeConfiguration
carsEntityReferenced Entity: Car, Relationship: ordered_one_to_many, Strong: ✓
customersEntityReferenced Entity: Customer, Relationship: ordered_one_to_many, Strong: ✓
salesEntityReferenced Entity: Sale, Relationship: ordered_one_to_many, Strong: ✓

You can also enable the List Model checkbox to generate reactive QAbstractListModel and its QML wrappers. Set Displayed Field to specify which field appears in list views (e.g., make for cars, name for customers).

Key concepts:

  • Strong relationship: Deleting Root cascades to delete all Cars, Customers, Sales

Step 5: Define Features and Use Cases

Click Features in the sidebar. You’ll see a four-column layout.

5.1 Create the Feature

  1. Click + next to “Features”
  2. Select it and set Name: inventory_management

5.2 Create the Import Use Case

  1. Click + next to “Use Cases”
  2. Configure:
FieldValue
Nameimport_inventory
Undoable(file imports typically aren’t undoable)
Read Only(it will update the internal database)
Long Operation

Note: Long Operation is currently implemented for Rust only. For now, C++ / Qt6 ignores this setting.

  1. Switch to the DTO In tab:

    • Enable the checkbox
    • Name: ImportInventoryDto
    • Add field: file_path (String)
  2. Switch to the DTO Out tab:

    • Enable the checkbox
    • Name: ImportResultDto
    • Add fields: imported_count (Integer), error_messages (String, List: ✓)
  3. Switch to the Entities tab:

    • Check: Root, Car

5.3 Create the Export Use Case

  1. Click + next to “Use Cases”
  2. Configure:
FieldValue
Nameexport_inventory
Undoable
Read Only(just reading internal data)
Long Operation

Note: Long Operation is currently implemented for Rust only.

  1. DTO In:

    • Name: ExportInventoryDto
    • Field: output_path (String)
  2. DTO Out:

    • Name: ExportResultDto
    • Field: exported_count (Integer)
  3. Entities: Check Root, Car

5.4 Choose your UI

For C++ / Qt6, several GUI are available. QtQuick, QtWidgets, Lomiri, … These options scaffold basic UI code that interacts with the generated controllers.

The controllers, models, and “singles” (like in “Single model”) C++ wrappers for integration with QML are generated for you. Also, mock implementations for each of these files are generated for you to allow developing the UI without the backend.

5.5 Save the Manifest

Click Save Manifest in the header (or Ctrl+S).

5.6 Take a break, drink a coffee, sleep a bit

I mean it. A fresher mind sees things more clearly. You already saved a lot of time by using Qleany instead of writing all the boilerplate yourself. Don’t rush the design phase, it’s where you get the most value from Qleany.

Designing your domain and use cases is the most important part. The generated code is a complete architecture, not mere scaffolding. If the model is wrong, the code won’t help much. Take your time to get it right before generating.

Yes, you can change the manifest and regenerate later. But it’s better to get a solid design upfront. The more you change the model after generating, the more work you create for yourself. It’s not a problem to evolve your design, but try to avoid major changes that require rewriting large parts of the generated code.


Step 6: Generate

Commit to Git

Before generating, commit your current state to Git. This isn’t optional advice — it’s how Qleany is meant to be used. If you accidentally overwrite files you’ve modified, you can restore them.

git add .
git commit -m "Before Qleany generation"

Generate Code

  1. Click Generate in the sidebar
  2. Review the groups and files
  3. (Optional) Check in temp/ to generate to a temporary folder first
  4. Click a file to preview the generated code
  5. Click Generate (N) where N is the number of selected files

The progress modal shows generation status. Files are written to your project.

The files are formatted with clang-format (Microsoft style).


Step 7: What You Get

After a generation, your project contains:

├── cmake
│   ├── InstallHelpers.cmake
│   └── VersionFromGit.cmake
├── CMakeLists.txt
└── src
    ├── common
    │   ├── CMakeLists.txt
    │   ├── controller_command_helpers.h
    │   ├── database
    │   │   ├── db_builder.h
    │   │   ├── db_context.h
    │   │   ├── junction_table_ops
    │   │   │   ├── junction_cache.h
    │   │   │   ├── many_to_one.cpp
    │   │   │   ├── many_to_one.h
    │   │   │   ├── one_to_one.cpp
    │   │   │   ├── one_to_one.h
    │   │   │   ├── ordered_one_to_many.cpp
    │   │   │   ├── ordered_one_to_many.h
    │   │   │   ├── unordered_many_to_many.cpp
    │   │   │   ├── unordered_many_to_many.h
    │   │   │   ├── unordered_one_to_many.cpp
    │   │   │   └── unordered_one_to_many.h
    │   │   └── table_cache.h
    │   ├── direct_access                    # Holds the repositories and table implementations
    │   │   ├── car
    │   │   │   ├── car_events.h
    │   │   │   ├── car_repository.cpp
    │   │   │   ├── car_repository.h
    │   │   │   ├── car_table.cpp
    │   │   │   ├── car_table.h
    │   │   │   ├── CMakeLists.txt
    │   │   │   ├── i_car_repository.h
    │   │   │   └── table_definitions.h
    │   │   ├── CMakeLists.txt
    │   │   ├── converter_registration.h
    │   │   ├── customer
    │   │   │   ├── CMakeLists.txt
    │   │   │   ├── customer_events.h
    │   │   │   ├── customer_repository.cpp
    │   │   │   ├── customer_repository.h
    │   │   │   ├── customer_table.cpp
    │   │   │   ├── customer_table.h
    │   │   │   ├── i_customer_repository.h
    │   │   │   └── table_definitions.h
    │   │   ├── event_registry.h                # event system for reactive updates
    │   │   ├── mapper_tools.h
    │   │   ├── operators.h
    │   │   ├── repository_factory.cpp
    │   │   ├── repository_factory.h
    │   │   ├── root
    │   │   │   ├── CMakeLists.txt
    │   │   │   ├── i_root_repository.h
    │   │   │   ├── root_events.h
    │   │   │   ├── root_repository.cpp
    │   │   │   ├── root_repository.h
    │   │   │   ├── root_table.cpp
    │   │   │   ├── root_table.h
    │   │   │   └── table_definitions.h
    │   │   └── sale
    │   │       ├── CMakeLists.txt
    │   │       ├── i_sale_repository.h
    │   │       ├── sale_events.h
    │   │       ├── sale_repository.cpp
    │   │       ├── sale_repository.h
    │   │       ├── sale_table.cpp
    │   │       ├── sale_table.h
    │   │       └── table_definitions.h
    │   ├── entities
    │   │   ├── car.h
    │   │   ├── CMakeLists.txt
    │   │   ├── customer.h
    │   │   ├── root.h
    │   │   └── sale.h
    │   ├── features
    │   │   ├── CMakeLists.txt
    │   │   ├── feature_event_registry.h           # event system for reactive updates
    │   │   └── inventory_management_events.h
    │   ├── service_locator.cpp
    │   ├── service_locator.h
    │   ├── undo_redo                              # undo/redo 
    │   │   ├── group_command_builder.cpp
    │   │   ├── group_command_builder.h
    │   │   ├── group_command.cpp
    │   │   ├── group_command.h
    │   │   ├── query_handler.cpp
    │   │   ├── query_handler.h
    │   │   ├── undo_redo_command.cpp
    │   │   ├── undo_redo_command.h
    │   │   ├── undo_redo_manager.cpp
    │   │   ├── undo_redo_manager.h
    │   │   ├── undo_redo_stack.cpp
    │   │   ├── undo_redo_stack.h
    │   │   ├── undo_redo_system.cpp
    │   │   └── undo_redo_system.h
    │   └── unit_of_work
    │       ├── unit_of_work.h
    │       ├── uow_base.h
    │       ├── uow_macros.h
    │       └── uow_ops.h
    ├── direct_access
    │   ├── car
    │   │   ├── car_controller.cpp        # Exposes CRUD operations to UI
    │   │   ├── car_controller.h
    │   │   ├── car_unit_of_work.h
    │   │   ├── CMakeLists.txt
    │   │   ├── dtos.h
    │   │   └── use_cases                 # The logic here is auto-generated
    │   │       ├── common
    │   │       │   └── dto_mapper.h
    │   │       ├── create_uc.cpp
    │   │       ├── create_uc.h
    │   │       ├── get_uc.cpp
    │   │       ├── get_uc.h
    │   │       ├── i_car_unit_of_work.h
    │   │       ├── remove_uc.cpp
    │   │       ├── remove_uc.h
    │   │       ├── update_uc.cpp
    │   │       └── update_uc.h
    │   ├── CMakeLists.txt
    │   ├── customer
    │   │   └── ...
    │   ├── root
    │   │   └── ...
    │   └── sale
    │       ├── CMakeLists.txt
    │       ├── dtos.h
    │       ├── sale_controller.cpp
    │       ├── sale_controller.h
    │       ├── sale_unit_of_work.h
    │       └── use_cases                  # The logic here is auto-generated
    │           ├── common
    │           │   └── dto_mapper.h
    │           ├── create_uc.cpp
    │           ├── create_uc.h
    │           ├── get_relationship_ids_count_uc.cpp
    │           ├── get_relationship_ids_count_uc.h
    │           ├── get_relationship_ids_in_range_uc.cpp
    │           ├── get_relationship_ids_in_range_uc.h
    │           ├── get_relationship_ids_many_uc.cpp
    │           ├── get_relationship_ids_many_uc.h
    │           ├── get_relationship_ids_uc.cpp
    │           ├── get_relationship_ids_uc.h
    │           ├── get_uc.cpp
    │           ├── get_uc.h
    │           ├── i_sale_unit_of_work.h
    │           ├── remove_uc.cpp
    │           ├── remove_uc.h
    │           ├── set_relationship_ids_uc.cpp
    │           ├── set_relationship_ids_uc.h
    │           ├── update_uc.cpp
    │           └── update_uc.h
    ├── inventory_management
    │   ├── CMakeLists.txt
    │   ├── inventory_management_controller.cpp    # Exposes operations to UI
    │   ├── inventory_management_controller.h
    │   ├── inventory_management_dtos.h
    │   ├── units_of_work                 # adapt the macros here 
    │   │   ├── export_inventory_uow.h
    │   │   └── import_inventory_uow.h
    │   └── use_cases
    │       ├── export_inventory_uc          # adapt the macros here 
    │       │   └── i_export_inventory_uow.h
    │       ├── export_inventory_uc.cpp      # You implement the logic here
    │       ├── export_inventory_uc.h
    │       ├── import_inventory_uc          # adapt the macros here 
    │       │   └── i_import_inventory_uow.h
    │       ├── import_inventory_uc.cpp      # You implement the logic here
    │       └── import_inventory_uc.h
    └── qtwidgets_app
        ├── CMakeLists.txt
        ├── main.cpp
        ├── main_window.cpp
        └── main_window.h

What’s generated:

  • Complete CRUD for all entities (create, get, update, remove, …)
  • Controllers exposing operations
  • DTOs for data transfer
  • Repository pattern for database access
  • Undo/redo infrastructure for undoable operations
  • Tests suites for the database and undo redo infrastructure
  • Macros for unit of work
  • Event system for reactive updates
  • Basic CLI (if selected during project setup)
  • Basic empty UI (if selected during project setup)

What you implement:

  • Your custom use case logic (import_inventory, export_inventory)
  • Your UI or CLI on top of the controllers or their adapters.

Step 8: Run the Generated Code

Let’s assume that you have Qt6 dev libs and QCoro-qt6 dev libs installed in the system. Also, install cmake and extra-cmake-modules.

You can use an IDE like Qt Creator or VS Code and build/run the project from there.

Or in a terminal,

mkdir build && cd build
cmake ..
cmake --build . --target all -j$(nproc)

Run the app (in case of QtWidgets):

./src/qtwidgets_app/CarLot

Next Steps

  1. Run the generated code — it compiles and provides working CRUD
  2. Implement your custom use cases (import_inventory, export_inventory)
  3. Build your UI on top of the controllers
  4. Add more features as your application grows

Tips

Understanding the Internal Database

Entities are stored in an internal database (SQLite). This database is internal, users and UI devs don’t interact with it directly.

Typical pattern:

  1. User opens a file (e.g., .carlot project file)
  2. Your load_project use case parses the file and populates entities
  3. User works — all changes go to the internal database
  4. User saves — your save_project use case serializes entities back to file

The internal database is ephemeral. It enables fast operations, undo/redo. The user’s file is the permanent storage.

Undo/Redo

Every generated CRUD operation supports undo/redo automatically. You don’t have to display undo/redo controls in your UI if you don’t want to, but the infrastructure is there when you need it.

If you mark a use case as Undoable, Qleany generates the command pattern scaffolding. You fill in what “undo” means for your specific operation.

For more information, see Undo-Redo Architecture.

Relationships

RelationshipUse When
one_to_oneExclusive 1:1 (User → Profile)
many_to_oneChild references parent (Sale → Car)
one_to_manyParent owns unordered children
ordered_one_to_manyParent owns ordered children (chapters in a book)
many_to_manyShared references (Items ↔ Tags)

Strong means cascade delete — deleting the parent deletes children.

For more details, see Manifest Reference.

Regenerating

Made a mistake? The manifest is just YAML. You can:

  • Edit it directly in a text editor or from the GUI tool
  • Delete entities/features in the UI and recreate them
  • Generate to a temp folder, review, then regenerate to the real location

For more details, see Regeneration Workflow.


The generated code is yours. Modify it, extend it, or regenerate when you add new entities. Qleany gets out of your way.


Further Reading

Design Philosophy

This document explains the architectural principles behind Qleany and why it generates code the way it does.

What is Clean Architecture?

Clean Architecture, introduced by Robert C. Martin, organizes code into concentric layers with strict dependency rules:

┌─────────────────────────────────────────┐
│            Frameworks & UI              │  ← Outer: Qt, QML, SQLite
├─────────────────────────────────────────┤
│          Controllers & Gateways         │  ← Interface adapters
├─────────────────────────────────────────┤
│              Use Cases                  │  ← Application business rules
├─────────────────────────────────────────┤
│              Entities                   │  ← Core: Enterprise business rules
└─────────────────────────────────────────┘

The Dependency Rule: Source code dependencies point inward. Inner layers know nothing about outer layers. Entities don’t know about use cases. Use cases don’t know about controllers. This makes the core testable without frameworks.

Key concepts Qleany retains:

  • Entities — Domain objects with identity and business rules
  • Features — Groupings of related use cases and entities
  • Use Cases — Single-purpose operations encapsulating business logic
  • DTOs — Data transfer objects crossing layer boundaries
  • Repositories — Abstractions over data access
  • Dependency Inversion — High-level modules don’t depend on low-level modules

The Problem with Pure Clean Architecture

Strict Clean Architecture organizes code by layer:

src/
├── domain/
│   └── entities/
│       ├── work.h
│       ├── car.h
│       └── car_item.h
├── application/
│   └── use_cases/
│       ├── work/
│       ├── car/
│       └── car_item/
├── infrastructure/
│   └── repositories/
│       ├── work_repository.h
│       └── car_repository.h
└── presentation/
    └── controllers/
        ├── work_controller.h
        └── car_controller.h

To modify “Car,” you touch four directories. For a 17-entity project, Qleany v0 generated 1700+ C++ files across 500 folders. Technically correct, practically unmaintainable.

Package by Feature (a.k.a. Vertical Slice Architecture)

Package by Feature groups code by what it does, not what layer it belongs to:

src/
├── common/                      # Truly shared infrastructure
│   ├── entities/
│   ├── database/
│   └── undo_redo/
└── direct_access/
    └── car/                  # Everything about Car in one place
        ├── car_controller.h
        ├── car_repository.h
        ├── dtos.h
        ├── unit_of_work.h
        └── use_cases/
            ├── create_uc.h
            ├── get_uc.h
            ├── update_uc.h
            └── remove_uc.h

To modify “Car,” you only touch one folder. It’s easier to find code, understand features, and make changes. For the same 17-entity project, Qleany now generates 600 files across 80 folders. Roughly, 33 files per entity instead of 90.

Benefits:

  • Discoverability — Find all Car code in one place
  • Cohesion — Related code changes together
  • Fewer files — Same 17-entity project produces ~600 files across ~80 folders
  • Easier onboarding — New developers understand features, not layers

Why Vertical Slices?

The term comes from visualizing your application as a layered cake. A horizontal slice would be one entire layer (all controllers, or all repositories). A vertical slice cuts through all layers for one feature — from UI down to database, but only for that specific capability.

Each slice is relatively self-contained. You can understand, modify, and test the Car feature without understanding how Events or Tags work internally. This isolation makes onboarding easier and reduces the blast radius of changes.

What We Keep from Clean Architecture

  • Dependency direction (UI → Controllers → Use Cases → Repositories → Database)
  • Use cases as the unit of business logic
  • DTOs at boundaries
  • Repository pattern for data access
  • Testability through clear interfaces

What We Drop

  • Strict layer-per-folder organization
  • Separate “domain” module (entities live in common)
  • Interface-for-everything (only where it aids testing)

Why This Matters for Desktop Apps

Web frameworks often provide architectural scaffolding (Rails, Django, Spring). Desktop frameworks like Qt provide widgets and signals, but little guidance on organizing a 50,000-line application.

Qleany fills that gap with an architecture that:

  • Scales from small tools to large applications
  • Integrates naturally with Qt’s object model
  • Supports undo/redo, a desktop-specific requirement
  • Keeps related code together for solo developers and small teams
  • Supports multiple UIs (Qt Widgets, QML, CLI) sharing the same core logic

For the complete file organization, see Generated Infrastructure - C++/Qt or Generated Infrastructure - Rust.

Why this Matters for Mobile Apps

Mobile apps share many characteristics with desktop apps (see above), but have additional constraints:

  • Rich UIs with complex interactions
  • Need for offline functionality
  • Local data storage with sync capabilities
  • Performance constraints requiring efficient architecture

For the performance, since Qleany generates C++ and Rust, it can be called performant enough for mobile apps. Mobile apps often require efficient memory usage and responsiveness, which C++ and Rust can provide.

A Rust backend could be plugged into a mobile app developed with native technologies (Swift for iOS, Kotlin for Android) or cross-platform frameworks (Flutter, React Native). This way, the core logic benefits from Rust’s performance and safety, while the UI is built with tools optimized for mobile platforms.

Generate and Disappear

Qleany generates code, then gets out of your way. The output has no dependency on Qleany itself. Modify, extend, or delete the generated code freely. The generated code is yours — there’s no runtime, no base classes to inherit from, no framework to learn.

No Framework, No Runtime

Qleany generates plain Rust structs and C++ classes. There’s no:

  • Base class you must inherit from
  • Trait you must implement for Qleany
  • Runtime library to link against

The generated code uses standard libraries (redb for Rust, Qt for C++) but has no Qleany-specific dependencies. If you decide to stop using Qleany, the generated code continues to work unchanged.

Manifest as Source of Truth

The qleany.yaml manifest defines your architecture. It’s:

  • Human-readable — Edit it directly when the UI is inconvenient
  • Version-controllable — Diff changes, review in PRs
  • Portable — Share between team members, regenerate on any machine

The manifest describes what you want. Qleany figures out how to generate it. When templates improve, regenerate from the same manifest to get updated code.

Rust Module Structure

Qleany generates Rust code using the modern module naming convention. Instead of:

direct_access/
└── car/
    └── mod.rs      # Old style

Qleany generates:

direct_access/
├── car.rs          # Module file
└── car/            # Submodules folder
    ├── controller.rs
    ├── dtos.rs
    └── use_cases.rs

This follows Rust’s recommended practice since the 2018 edition, avoiding the proliferation of mod.rs files that makes navigation difficult.

Code quality and “purity”

Qleany deliberately generates straightforward code. A developer with only a few years of experience in C++ or Rust should be able to understand and modify it.

In practice, for Rust this means:

  • lifetimes only where the compiler requires them (no complex multi-lifetime scenarios), mostly deep inside the infrastructure
  • no async/await
  • generics only from standard library types (Result, Option, Vec) — no custom generic abstractions
  • no unsafe code
  • more cloning than strictly necessary
  • generated traits stay simple
  • the only macro exists to help the developer with custom units of work

For C++/Qt:

  • some C++20 aggregates and std::optional
  • exceptions used for error handling
  • async operations handled through QCoro where the event loop requires it
  • no raw pointers, only smart pointers
  • no multi-level inheritance, be it virtual or polymorphic
  • more copying than strictly necessary, though std::move is used deeper inside the infrastructure

This is a deliberate trade-off between approachability and performance. Qleany prioritizes code that intermediate developers can confidently modify over code that squeezes every last microsecond from the CPU. The generated code is clean, readable, and maintainable. You are using Rust or C++, two fast languages, and you are not writing a game engine.

In most desktop and mobile applications, the time spent waiting for user input or database access dwarfs any overhead from an extra clone. The few microseconds lost to cloning a DTO are rarely the bottleneck, but code that’s too clever for the team to maintain can be.

If you need every optimization, write your hot paths by hand. Profile first, then optimize what matters. The generator gives you a solid, maintainable baseline to build on.

Plugins

I add this little section about plugins too while I’m at it. Qt plugins especially. To paraphrase Uncle Bob: “UI is a detail, database is a detail”, … and plugins are details too. They can change without affecting the core business rules. The entities, use cases, don’t care whether you’re using a SQLite database or a JSON file. They don’t care whether the UI is QML or something else. This is the same idea with plugins. Plugin realm is outside the core (entities and business rules).

In concrete terms, this means that the plugin system is implemented in the outermost layer (Frameworks & UI). The core application logic doesn’t depend on plugins. Instead, plugins depend on the core application logic. This way, you can add, remove, or change plugins without affecting the core functionality of your application.

If I had to create an application using plugins, I would design entities dedicated to managing plugins and their data, a feature dedicated to plugins. Maybe a feature by plugin type to be compartmentalized. Consider these features/use cases as the API for plugins to interact with the core application. The core application would provide services and data to the plugins through these use cases, ensuring that plugins can operate independently of the core logic.

Also, I’d separate the plugins extending the UI from the plugins extending the backend logic. The UI plugins would be loaded and managed by the UI layer, while the backend plugins would exist in their own section, always in the outermost layer, separate from the UI. And all plugins can have access to the features/use cases dedicated to plugins.

User settings and UI configuration

This part may be obvious to most developers. Does the user settings/configuration belong to the core application logic? No, it doesn’t. It belongs to the outermost layer (Frameworks & UI). The core application logic should be agnostic of how settings are stored or managed. The settings/configuration system should be implemented in the outer layer, allowing the core logic to remain unaffected by changes in how settings are handled.

You don’t want the window geometry to be held in entities. Its place is in the UI layer. You don’t want the theme preference to be held in use cases. Its place is in the UI layer too. The core application logic should focus on business rules and data management, while settings and configuration are handled separately in the outer layer.

The business rules (= entities + use cases) can manage UI-agnostic settings, like user preferences that affect the behavior of the application but are not directly related to the UI. For example, the core logic can manage a setting that determines how data is processed or how certain features behave. But anything directly related to the UI should be kept in the UI layer.

In a perfect world, the use cases would stay pure and repeatable. They should not depend on user-specific settings or configurations. If a use case needs to behave differently based on user settings, it should receive those settings as input parameters, rather than accessing them directly. This keeps the use cases decoupled from the settings system, maintains their reusability, and keeps them testable.

This is not a perfect world. In practice, you can add a ISettings interface to the use case, the same way that the unit of work interface is made accessible to the use case. This way, the use case can access the settings it needs without being tightly coupled to the settings implementation. The settings system can be implemented in the outer layer, and the use cases can interact with it through a well-defined interface, maintaining separation of concerns and keeping the core logic clean.

Online APIs, databases, and other external services

Consider the application’s internal database as local and private. How do you handle data that needs to be synced with an external API or a remote database?

If the application needs to interact with an external API or a remote database, it should be done through a dedicated service or repository layer. This layer should handle the communication with the external system and provide a clean interface IRemoteWhatever for the use cases to interact with (like it’s already done with the units of work).

Need to check any update from this API? Create a dedicated use case to fetch the data from the API and update the internal database and/or act on the answer. This use case would be called periodically to check for updates. Typically, the UI layer would handle the timed loop that calls this use case every few seconds or minutes.

Example: a calendar application that syncs with an external calendar API.

  • the RemoteWhatever service is instantiated in the UI layer (typically instantiated in main.cpp and stored inside ServiceLocator or directly instantiated in the controller, just before calling the use case if it’s stateless)
  • the UI calls the use case every few seconds to check for updates
  • the use case calls the service from IRemoteWhatever to fetch the data from the API
  • the service updates the local database with the new data
  • the UI is notified of the changes and updates its UI accordingly
  • the use case can also trigger notifications or reminders based on the new data

Examples of services:

  • calling terminal commands
  • fetching data from an API
  • sending emails
  • sending push notifications
  • communicate with another application

When a use case is bound to use a specialized library, avoid putting the library-specific code directly in the use case. Instead, create an interface that abstracts away the library, and implement that interface in a separate class. This way, the use case remains decoupled from the specific library, making it easier to test and maintain. The implementation of the interface can be done in the outer layer, allowing you to swap out libraries or change implementations without affecting the core logic of the use case.

Entity Tree and Undo-Redo Architecture

Undo-redo systems are harder than they first appear. A robust implementation must handle complex scenarios like nested entities, cascading changes, and maintaining data integrity during undos and redos. Qleany’s undo-redo architecture is designed to simplify these challenges by enforcing clear rules on how entities relate to each other in the context of undo-redo operations.

Entities in Qleany form a tree structure based on strong (ownership) relationships. This tree organization directly influences how undo-redo works across your application.

Quick Reference

Before diving into the details, here is a summary of the two approaches Qleany supports:

AspectApproach A: Document-ScopedApproach B: Panel-Scoped
Stack lifecycleCreated when document opens, destroyed when it closesCreated when panel gains focus, cleared on focus loss or after undo
History depthUnlimitedOne command
Redo behaviorFull redo history until new actionSingle-use, lost on focus change
Deletion handlingOptional stack-based or soft-delete with toastSoft-delete with timed toast
User expectation“Undo my last change to this document”“Undo my immediate mistake”
Best forIDEs, creative suites, document editorsForm-based apps, simple tools

Qleany itself uses Approach B. Skribisto uses Approach A.

My Recommendations

Do not use a single, linear undo-redo stack for the entire application except in the most basic cases. As Admiral Ackbar said: “It’s a trap!”

Think about interactions from the user’s perspective: they expect undo and redo to apply to specific contexts rather than globally. A monolithic stack leads to confusion and unintended consequences. If a user is editing a document and undoes an action, they do not expect that to also undo changes in unrelated settings or other documents.

Instead, each context should have its own undo-redo stack. The question is how to define “context.” Qleany supports two approaches, described below, suited to different application types. Both use the same generated infrastructure; they differ only in when and where stacks are created and destroyed.

For destructive operations such as deleting entities, Qleany supports cascading deletions and their undoing. If you delete a parent entity, all its strongly-owned children are also deleted, and you can undo that. At first, I used a database savepoint to be restored on undo, but the savepoint impacted non-undoable data as well, leading to confusion and unexpected behavior. Now, the create, createOrphans, remove and setRelationshipsIds commands use cascading snapshots of the individual tables to restore the database state before the operation.

Yet this behavior may be not what the user expects. Instead, you can use soft-deletion with timed recovery, described in the Soft Deletion section below.

Two Approaches to Undo-Redo

Approach A: Document-Scoped Stack

The stack is created when a document, workspace, or undoable trunk is loaded and destroyed when it closes. All UI panels editing entities within that trunk share the same stack.

This approach provides full undo history across the entire document. When the user presses Ctrl+Z, the application undoes the most recent change to the document regardless of which panel made it. This matches the behavior of professional tools like Qt Creator, Blender, and Adobe applications.

Redo works symmetrically: the user can redo any undone action until they perform a new action, which clears the redo stack.

Lifecycle. Create the stack when the document opens. Destroy it when the document closes. All panels resolve the same stack_id by looking up the document they are editing.

User expectation. “Undo my last change to this document.”

Best suited for. Complex applications, professional tools, creative suites, IDEs, and any application where users expect deep undo history and work on persistent documents over extended sessions.

Approach B: Panel-Scoped Stack, Length 1

The stack is created when a panel becomes active and cleared or destroyed when the panel loses focus or after a single undo executes. Each panel manages its own short-lived stack holding at most one command.

This approach provides immediate mistake recovery without maintaining history. When the user presses Ctrl+Z, the application undoes only their most recent action in that panel. After one undo, the stack is empty. This matches modern application patterns where undo is an “oops” button rather than a time-travel mechanism.

Redo is effectively single-use in this approach. After undoing, the user can redo immediately, but switching focus or performing any new action clears the redo slot. This is an acceptable trade-off for the simplicity gained.

Lifecycle. Create the stack when the panel gains focus. Clear or destroy it when the panel loses focus or after undo executes.

User expectation. “Undo my immediate mistake.”

Best suited for. Simpler applications, form-based interfaces, and applications where deep undo history would cause more confusion than benefit.

Entity Properties

With the approach chosen, configure your entities using these properties relevant to undo-redo:

PropertyTypeDefaultEffect
undoableboolfalseAdds undo/redo support to the entity’s controller
allow_direct_accessbooltrueGenerates entity files in direct_access/ for UI access
single_modelboolfalseGenerates Single{Entity} wrapper for QML (C++/Qt only)

Undo-Redo Rules

The undo-redo system follows strict inheritance rules through the entity tree:

  1. A non-undoable entity cannot have an undoable entity as parent (strong relationship)
  2. All children of an undoable entity must also be undoable
  3. Weak relationships (references) can point to any entity regardless of undo status

These rules ensure that when you undo an operation on a parent entity, all its strongly-owned children can be consistently rolled back.

What happens if you violate these rules? The code will generate, compile, and run — Qleany does not enforce these rules at generation time. However, undo/redo stacks will become inconsistent. For example, if you place non-undoable persistent settings as a child of an undoable entity, those settings could be unexpectedly undone by cascade when the user undoes the parent. You do not want application settings disappearing because the user undid an unrelated action.

Follow these rules strictly. If data should not participate in undo (like settings), place it in a separate non-undoable trunk — do not nest it under undoable entities.

A basic validation system checks some of these rules at generation time. It is being improved to perform checks at load time as well.

Entity Tree Configurations

Depending on your application’s complexity, you can organize your entity tree in three ways.

Configuration 1: No Undo-Redo

For simple applications where undo-redo is not needed, all entities are non-undoable.

Root (undoable: false)
├── Settings
├── Project
│   ├── Document
│   └── Asset
└── Cache
entities:
  - name: Root
    inherits_from: EntityBase
    undoable: false
    fields:
      - name: settings
        type: entity
        entity: Settings
        relationship: one_to_one
        strong: true
      - name: projects
        type: entity
        entity: Project
        relationship: ordered_one_to_many
        strong: true

Even without user-facing undo-redo, the undo system must be initialized internally as it is used for transaction management.

Configuration 2: Single Undoable Trunk

For applications where all user data should support undo-redo, the root is non-undoable with a single undoable trunk beneath it.

Root (undoable: false)
└── Workspace (undoable: true)     ← All user data under this trunk
    ├── Project (undoable: true)
    │   ├── Document (undoable: true)
    │   └── Asset (undoable: true)
    └── Tag (undoable: true)
entities:
  - name: Root
    inherits_from: EntityBase
    undoable: false
    fields:
      - name: workspace
        type: entity
        entity: Workspace
        relationship: one_to_one
        strong: true

  - name: Workspace
    inherits_from: EntityBase
    undoable: true
    fields:
      - name: projects
        type: entity
        entity: Project
        relationship: ordered_one_to_many
        strong: true
      - name: tags
        type: entity
        entity: Tag
        relationship: one_to_many
        strong: true

  - name: Project
    inherits_from: EntityBase
    undoable: true
    fields:
      - name: documents
        type: entity
        entity: Document
        relationship: ordered_one_to_many
        strong: true

With Approach A, create one stack when the Workspace loads. All panels share this stack, and the user has full undo history across the entire workspace.

With Approach B, each panel creates and manages its own stack independently. The user has immediate undo within each panel, with deletions handled via toast notifications.

Configuration 3: Multiple Trunks

For applications that need both undoable user data and non-undoable system data, or for multi-document applications where each document should have independent undo history, the root has multiple trunks.

Root (undoable: false)
├── System (undoable: false)       ← Non-undoable trunk
│   ├── Settings (undoable: false)
│   ├── RecentFiles (undoable: false)
│   └── SearchResults (undoable: false)
│
└── Workspace (undoable: true)     ← Undoable trunk
    ├── Event (undoable: true)
    │   └── Attendee (undoable: true)
    └── Calendar (undoable: true)

For multi-document applications:

Root (undoable: false)
├── System (undoable: false)
├── Document A (undoable: true)    ← Stack A
├── Document B (undoable: true)    ← Stack B
└── Document C (undoable: true)    ← Stack C
entities:
  - name: Root
    inherits_from: EntityBase
    undoable: false
    fields:
      - name: system
        type: entity
        entity: System
        relationship: one_to_one
        strong: true
      - name: workspace
        type: entity
        entity: Workspace
        relationship: one_to_one
        strong: true

  - name: System
    inherits_from: EntityBase
    undoable: false
    allow_direct_access: true
    fields:
      - name: settings
        type: entity
        entity: Settings
        relationship: one_to_one
        strong: true
      - name: recentFiles
        type: entity
        entity: RecentFile
        relationship: ordered_one_to_many
        strong: true
      - name: searchResults
        type: entity
        entity: SearchResult
        relationship: one_to_many
        strong: true

  - name: Settings
    inherits_from: EntityBase
    undoable: false
    fields:
      - name: theme
        type: string
      - name: language
        type: string

  - name: SearchResult
    inherits_from: EntityBase
    undoable: false
    allow_direct_access: false
    fields:
      - name: query
        type: string
      - name: matchedItem
        type: entity
        entity: Event
        relationship: many_to_one

  - name: Workspace
    inherits_from: EntityBase
    undoable: true
    fields:
      - name: events
        type: entity
        entity: Event
        relationship: ordered_one_to_many
        strong: true
      - name: calendars
        type: entity
        entity: Calendar
        relationship: one_to_many
        strong: true

  - name: Event
    inherits_from: EntityBase
    undoable: true
    single_model: true
    fields:
      - name: title
        type: string
      - name: attendees
        type: entity
        entity: Attendee
        relationship: one_to_many
        strong: true
        list_model: true

With Approach A, each document gets its own stack. Ctrl+Z in Document A’s editor undoes only Document A’s changes. This provides natural contextual undo at the document level.

With Approach B, the multi-document structure is less relevant since each panel manages its own immediate-undo stack regardless of which document it edits.

Here is the section, written to sit between Configuration 3 and Cross-Trunk References:


Breaking the Mold

The three configurations above are the patterns I recommend and use myself. They are not the only ones the infrastructure supports.

Qleany’s generated code does not enforce a single Root entity. It does not enforce tree-structured ownership at all. The repository layer provides createOrphans alongside create. The undo/redo system keys its stacks by integer ID, not by position in a tree. The snapshot/restore system captures whatever entity graph it finds. Nothing checks that your entities form a coherent tree at runtime.

This means you can do things the configurations above don’t show:

Multiple independent roots. You can create several root-like entities, each owning a separate subtree with its own undo stack. Think of a multi-workspace IDE where each workspace is truly independent — its own entities, its own undo history, no shared state. This works. I haven’t needed it in Skribisto or Qleany, but the infrastructure won’t stop you.

Flat orphan entities. You can skip the tree model entirely and use createOrphans for everything, managing relationships through weak references. For a simple utility with a handful of entities and no undo/redo, this is less ceremony than setting up a Root → Workspace hierarchy you don’t need.

Hybrid approaches. A tree for your main domain model, orphan entities for transient data that doesn’t belong in the tree. The infrastructure doesn’t care.

So why do I recommend the tree model so insistently?

Because the tree model gives you things for free that you must handle manually without it. Cascade deletion follows ownership: delete a parent, all strongly-owned children are deleted. Snapshot/restore captures the full subtree: undo a deletion, everything comes back including nested children and their junction relationships. Undo stack scoping maps naturally to tree branches: one stack per document, one stack per workspace.

Without the tree, you take on these responsibilities yourself. Orphan entities have no owner to cascade from, you must track and delete them explicitly. A parent’s snapshot does not capture entities outside a tree, you must manage their lifecycle in your use case logic. Undo stack assignment becomes your problem rather than a natural consequence of the data structure.

None of this is impossible. It’s just work that the tree model handles for you.

If you deviate from the prescribed configurations, the undo/redo rules from the previous section still apply. A non-undoable entity should not be strongly owned by an undoable entity, regardless of your tree topology. The infrastructure won’t warn you. The undo stacks will just become inconsistent, and you’ll spend an afternoon figuring out why.

My advice: start with the tree model. If you later find it too rigid for a specific part of your application, relax it locally — use orphans for that part, keep the tree for the rest. Don’t start with a flat model and try to add structure later. It’s easier to remove structure than to add it.


Cross-Trunk References

Non-undoable entities can hold weak references (many_to_one, many_to_many) to undoable entities. This is useful for search results, recent items, or bookmarks that point to user data without owning it.

- name: SearchResult
  undoable: false
  fields:
    - name: matchedEvent
      type: entity
      entity: Event
      relationship: many_to_one

The reverse is also true: undoable entities can reference non-undoable entities, such as referencing a Settings entity for default values.

Soft Deletion

Definition: deletions are handled outside the undo stack using soft-deletion with timed hard-deletion.

To implement soft deletion, add an activated boolean field to your entities. When “deleting” an entity, set this flag to false instead of removing it from the database. Your UI filters out entities where activated is false, effectively hiding them from the user.

For immediate recovery, display a toast notification with an “Undo” action for a few seconds after deletion, typically three seconds. Maintain a timer for each soft-deleted entity. If the user clicks “Undo” within the timeout window, restore the entity by setting activated back to true and cancel the timer. If the timeout expires, perform the hard-delete.

This pattern is time-bounded rather than focus-bounded. The user can switch panels, notice the toast still visible, and click “Undo” within the window. It matches user expectations from applications like Gmail, Slack, and Notion.

For longer-term recovery, you can implement a trash bin with a dedicated entity that holds references to soft-deleted items:

Idtrashed_dateentity_typeentity_id
12024-01-01Document42
22024-01-02Car7

Users can then restore items from the trash bin or permanently delete them. Permanently emptying the trash clears all undo-redo stacks, which is acceptable since permanent deletion is a non-undoable action from the user’s perspective as well.

For Approach A, you may alternatively implement deletion undo through the stack if your application requires full undo history for deletions, but the soft-deletion pattern remains simpler and avoids cascade-reversal complexity.

Note : Soft deletion isn’t baked-in to Qleany’s generated code. You must implement the activated field, filtering logic, toast UI, and timer management yourself. I only provide this pattern as a recommended best practice. To only display non-deleted entities, you can use QAbstractProxyModel in C++/Qt or filter models in QML.

Choosing Your Approach

The key questions to ask are: Do you have data that should not participate in undo? Do users expect deep history or just immediate mistake recovery? Will users work on multiple independent documents simultaneously?

Application TypeEntity ConfigurationRecommended Approach
Simple utilityNo undo-redoNeither
Form-based appSingle undoable trunkApproach B
Document editorSingle undoable trunkApproach A
Multi-document IDEMultiple undoable trunksApproach A
Creative suiteMultiple undoable trunksApproach A

Settings, preferences, search results, and caches belong in non-undoable trunks. User-created content belongs in undoable trunks. Temporary UI state belongs outside the entity tree entirely or in non-undoable trunks.


For implementation details of the undo/redo system including command infrastructure, async execution, and composite commands, see Generated Infrastructure - C++/Qt or Generated Infrastructure - Rust.

Regeneration Workflow

This document explains how Qleany handles file generation and what happens when you regenerate code.

The GUI is a convenient way to generate files selectively.

The Golden Rule

Generated files are overwritten when you regenerate them. Qleany does not merge changes or preserve modifications.

This is intentional. The workflow assumes you control what gets regenerated.

The GUI is helping you by checking the “in temp” checkbox by default to avoid accidental overwrites.

Before You Generate

Commit to Git first. This isn’t optional advice. It’s how the tool is meant to be used. If something goes wrong, you can recover. If you accidentally overwrite modified files, you can restore them. Yes, it happened to me, and it was painful.

Controlling What Gets Generated

In the UI

The Generate tab shows all files that would be generated. You select which ones to actually write:

  1. Click List Files to populate the file list
  2. Use group checkboxes to select/deselect categories
  3. Uncheck any files you’ve modified and want to keep
  4. Click Generate (N) to write only selected files

In the CLI

Inside the project folder, run:

# Generate all files (dangerous if you've modified any)
qleany generate

# Generate to temp folder first (safe)
qleany generate --temp

# Then compare and merge manually
diff -r ./temp/crates ./crates

# or for VS Code users:
code --diff ./temp/file ./file

What Happens When You Regenerate

  • Selected files are overwritten — Your modifications are lost
  • Unselected files are untouched — Even if the manifest changed
  • No files are deleted — If you rename an entity, the old files remain; clean them up manually

From the GUI (recommended), the “in temp” checkbox is checked by default to avoid accidental overwrites.

Files That Must Stay in Sync

When you add or remove an entity, certain files reference all entities and must be regenerated together. If you’ve modified one of these files, you’ll need to manually merge the changes.

Rust

These files contain references to all entities:

FileContains
common/event.rsEvent enum variants for all entities
common/entities.rsAll entity structs
common/direct_access/repository_factory.rsFactory methods for all repositories
common/direct_access/setup.rsFactory methods for all repositories
common/direct_access.rsModule declarations for all entity repositories
direct_access/lib.rsModule declarations for all entity features

C++/Qt

FileContains
common/database/db_builder.hDatabase table builder for all entities
common/direct_access/repository_factory.h/.cppFactory methods for all repositories
common/direct_access/event_registry.hEvent objects for all entities
common/entities/CMakeLists.txtAdds all entity source files to build
direct_access/CMakeLists.txtAdds all entity source files to build

If you modify one of these files and later add a new entity, you’ll need to either:

  • Regenerate the file and re-apply your modifications, or
  • Manually add the new entity references yourself

Using the Temp Folder

It’s recommended to add the “temp/” folder to your .gitignore.

The safest workflow when you’ve modified generated files:

  1. Check in temp/ checkbox in the UI (or use --temp or --output ./whatever/ in CLI)
  2. Generate all files to the temp location
  3. Compare temp output against your current files:
    diff -r ./temp/crates ./crates
    
    # or for VS Code users:
    code --diff ./temp/file ./file
    
  4. Manually merge changes you want to keep
  5. Delete the temp folder

This manual merge is the cost of customization. For files you modify heavily, consider whether the customization belongs in a separate file that won’t conflict with generation.

Practical Guidelines

Files you’ll typically regenerate freely

These are pure scaffolding with no business logic:

  • Entity structs (common/entities/)
  • DTOs (dtos.rs, dtos.h)
  • Repository implementations
  • Table/cache definitions
  • Event classes

Files you’ll typically modify and protect

These contain your custom code:

  • Use case implementations (your business logic)
  • Controllers (if you add custom endpoints)
  • Main entry point (main.rs, main.cpp)

Files that aggregate others

These need careful handling — regenerate them when adding entities, but be aware they may need manual merging:

  • Module declarations (lib.rs, feature exports)
  • Factory classes
  • Event registries

When You Rename an Entity

Qleany doesn’t track renames. If you rename Car to Vehicle:

  1. Update the manifest with the new name
  2. Generate the new Vehicle files
  3. Manually delete the old Car files
  4. Update any code that referenced Car

The old files won’t be removed automatically because Qleany never deletes files.

When Templates Improve

When Qleany’s templates are updated (new features, bug fixes, better patterns):

  1. Generate to temp folder with the new version
  2. Compare against your existing generated files
  3. Decide which improvements to adopt
  4. For files you haven’t modified: regenerate directly
  5. For files you’ve modified: merge manually or regenerate and re-apply your changes

The manifest remains your source of truth. The same manifest with improved templates produces better output.

Manifest Reference

Everything in Qleany is defined in qleany.yaml. This document covers all manifest options. The UI is still the primary way to create and edit manifests, but this reference helps when you need to edit the file directly. Or if you are curious.

Example Manifests

Real-world manifests you can reference:

ProjectLanguageFrontendLink
SkribistoC++20 / Qt6QtQuickqleany.yaml
QleanyRust 2024Slint + CLIqleany.yaml

Basic Structure

schema:
  version: 2

global:
  language: cpp-qt          # rust, cpp-qt
  application_name: MyApp
  organisation:
    name: myorg
    domain: myorg.com
  prefix_path: src

entities:
  - name: EntityBase
    # ...

features:
  - name: my_feature
    # ...

Required Base Entity

All entities must have id, created_at, and updated_at fields. These are essential for identity, caching, and change tracking.

To simplify this, Qleany provides EntityBase — a heritable entity with these three fields pre-defined. When you create a new manifest, Qleany automatically generates:

  • EntityBase with id, created_at, updated_at
  • An empty Root entity inheriting from EntityBase

All your entities should inherit from EntityBase using the inherits_from field.

entities:
  # EntityBase provides the necessary id, created_at, updated_at
  - name: EntityBase
    only_for_heritage: true
    allow_direct_access: false
    fields:
      - name: id
        type: uinteger
      - name: created_at
        type: datetime
      - name: updated_at
        type: datetime
        
  - name: Root
    inherits_from: EntityBase
    undoable: false
    fields:
      # Your root-level relationships here

Complete Example

schema:
  version: 2

global:
  language: cpp-qt
  application_name: MyApp
  organisation:
    name: myorg
    domain: myorg.com
  prefix_path: src

entities:
  - name: EntityBase
    only_for_heritage: true
    allow_direct_access: false
    fields:
      - name: id
        type: uinteger
      - name: created_at
        type: datetime
      - name: updated_at
        type: datetime
        
  - name: Root
    inherits_from: EntityBase
    undoable: false
    fields:
      - name: works
        type: entity
        entity: Work
        relationship: ordered_one_to_many
        strong: true
        list_model: true
        list_model_displayed_field: title

  - name: Work
    inherits_from: EntityBase
    undoable: true
    single_model: true
    fields:
      - name: title
        type: string
      - name: binders
        type: entity
        entity: Binder
        relationship: ordered_one_to_many
        strong: true
        list_model: true
        list_model_displayed_field: name
      - name: tags
        type: entity
        entity: BinderTag
        relationship: one_to_many
        strong: true

  - name: Binder
    inherits_from: EntityBase
    undoable: true
    fields:
      - name: name
        type: string
      - name: items
        type: entity
        entity: BinderItem
        relationship: ordered_one_to_many
        strong: true
        list_model: true
        list_model_displayed_field: title

  - name: BinderItem
    inherits_from: EntityBase
    undoable: true
    fields:
      - name: title
        type: string
      - name: parentItem
        type: entity
        entity: BinderItem
        relationship: many_to_one
      - name: tags
        type: entity
        entity: BinderTag
        relationship: many_to_many

features:
  - name: work_management
    use_cases:
      - name: load_work
        undoable: false
        entities: [Root, Work, Binder, BinderItem]
        dto_in:
          name: LoadWorkDto
          fields:
            - name: file_path
              type: string
        dto_out:
          name: LoadWorkResultDto
          fields:
            - name: work_id
              type: integer

Entity Options

OptionTypeDefaultDescription
namestringrequiredEntity name (PascalCase)
inherits_fromstringnoneParent entity for inheritance
only_for_heritageboolfalseEntity used only as base class
undoableboolfalseEnable undo/redo for this entity’s controller
allow_direct_accessbooltrueGenerate files in direct_access/ for UI access
single_modelboolfalseGenerate Single{Entity} QML wrapper (C++/Qt only)

Field options

OptionTypeDefaultDescription
namestringrequiredField name (snake_case)
typestringrequiredField type (see below)
entitystringnoneFor entity type, name of the entity
relationshipstringnoneFor entity type, relationship type (see below)
optionalboolfalseFor one_to_one and many_to_one
strongboolfalseFor one_to_one, one_to_many, and ordered_one_to_many, enable cascade deletion
list_modelboolfalseFor C++/Qt only, generate a C++ QAbstractListModel and its QML wrapper for this relationship field
list_model_displayed_fieldstringnoneFor C++/Qt only, default display role for the generated ListModel
enum_namestringnoneFor enum type, name of the enum (PascalCase)
enum_valuesarraynoneFor enum type, list of enum values (PascalCase)

Field Types

TypeDescriptionExample
booleanTrue/false valueis_active: true
integerWhole numbercount: 42
floatDecimal numberprice: 19.99
stringTextname: "Alice"
uuidUnique identifierid: "550e8400-..."
datetimeDate and timecreated_at: "2024-01-15T10:30:00"
entityRelationship to another entitySee relationship section
enumEnumerated valueSee enum section

Enum Fields

- name: status
  type: enum
  enum_name: CarStatus
  enum_values:
    - Available
    - Reserved
    - Sold

Like entities, the enum name should be PascalCase. Enum values should also be PascalCase. And the name must be unique.


Relationship Fields

This section will seem to be a bit repetitive, but for those not familiar with database relationships, it’s important to get all the details right. And some people have different ways of understanding relationships, so I want to be as clear as possible. Bear with me.

When type: entity, additional options define the relationship:

Relationship Types

RelationshipJunction TypeReturn Type (C++ / Rust)
one_to_oneOneToOnestd::optional<int> / Option<i64>
many_to_oneManyToOnestd::optional<int> / Option<i64>
one_to_manyUnorderedOneToManyQList<int> / Vec<i64>
ordered_one_to_manyOrderedOneToManyQList<int> / Vec<i64>
many_to_manyUnorderedManyToManyQList<int> / Vec<i64>

Relationship Flags

FlagValid forEffect
optionalone_to_one, many_to_oneValidated on create/update (0..1 instead of 1..1)
strongone_to_one, one_to_many, ordered_one_to_manyCascade deletion — removing parent removes children

QML Generation Flags (C++/Qt only)

FlagEffect
list_modelGenerate {Entity}ListModelFrom{Parent}{Relationship}
list_model_displayed_fieldDefault display role for the list model

Validation Rules

Flagone_to_onemany_to_oneone_to_manyordered_one_to_manymany_to_many
optional✓/✗✓/✗N.A.N.A.N.A.
strong✓/✗ (see note)N.A.✓/✗✓/✗N.A.

N.A.: Not applicable for this relationship type. There will be no change in generated code, or don’t use them. When you write code, an empty list expresses the intent to hold no relationship.

Note: If one_to_one holds a weak relationship (strong: false), it couldn’t be required (so, it must be optional: true). There is the risk of a dangling reference if the entity targeted by the reference is deleted.

Some invalid combinations are rejected at manifest parsing. This validation step is still in the works.


Understanding Relationships

Database relationships describe how entities connect. Two concepts matter:

Cardinality — How many entities can participate on each side?

  • One — At most one entity (0..1 or exactly 1)
  • Many — Zero or more entities (0..*)

Direction — Which side “owns” the relationship?

  • The parent side holds the list of children
  • The child side holds a reference back to its parent (in Qleany case, this reference is hidden from the user)

The reality in Qleany is a bit more nuanced, but this mental model helps understand how to model your data.

┌─────────────────────────────────────────────────────────────┐
│                     RELATIONSHIP TYPES                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ONE-TO-ONE (1:1)                                          │
│   ┌───────┐         ┌───────┐                               │
│   │ User  │─────────│Profile│   Each user has one profile   │
│   └───────┘         └───────┘   Each profile belongs to     │
│                                 one user                    │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ONE-TO-MANY (1:N)                                         │
│   ┌───────┐         ┌───────┐                               │
│   │Group  │────────<│ Item  │   One group  has many items   │
│   └───────┘         └───────┘   Binder.items: [1, 2, 3]     │
│                                                             │
│   MANY-TO-ONE (N:1)                                         │
│   ┌───────┐         ┌───────┐                               │
│   │ Car   │>────────│Brand  │   Many items belong to one    │
│   └───────┘         └───────┘   brand.  Car.brand: 5        │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   MANY-TO-MANY (N:M)                                        │
│   ┌───────┐         ┌───────┐                               │
│   │ Item  │>───────<│  Tag  │   Items have many tags        │
│   └───────┘         └───────┘   Tags apply to many items    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Reality in Qleany

Like said earlier, the reality is a bit more nuanced:

  • Special junction tables are used for all relationships (even the simpler ones) and “sit” between parent and child tables
  • These junction tables can be accessed by parent and child tables equally.
  • This means that for every relationship, both sides can see each other (no true “back-reference” concept)
  • The relationship type defines how the junction table behaves, and how the parent and child entities see each other.
  • In the deeper code, there is always the mentions of a left entity and a right entity (parent and child respectively in the mental model).

This may be easier to understand: all relationships are defined from the perspective of the entity holding the field. This means that:

  • For one_to_one, the entity with the field is one side, the referenced entity is the other side. Car.brand: EntityId
  • For many_to_one, the entity with the field is the “many” side (several entities of the same type), the referenced entity is the “one” side. Car.brand: EntityId
  • For one_to_many and ordered_one_to_many, the entity with the field is the “one” side, the referenced entity is the “many” side. Car.brand: Vec<EntityId>
  • For many_to_many, both sides are “many”. Car.brand: Vec<EntityId>

Why this design? The manifest describes meaning: ownership, cardinality, ordering, whether deletion cascades. The generated code handles implementation: junction tables, column schemas, query patterns, cache invalidation. You think about your domain; the generator thinks about the database. These are deliberately separated.

The cost is storage overhead. A one_to_one that could be a single foreign key column gets its own junction table instead. For a desktop or mobile application with hundreds or thousands of entities, you will never notice. For a web backend serving millions of rows, you would care, but that’s not what Qleany targets.

The payoff is uniformity. Every relationship, regardless of type, goes through the same junction table infrastructure. This means one code path for snapshot/restore (which is what makes undo/redo work on entity trees), one code path for bidirectional navigation (both sides can always see each other), and one code path for the generator to produce. No special cases, no “this relationship is simple enough for a foreign key but that one needs a junction table.” The complexity stays in the infrastructure, not in your head.

Yes, database engineers might cringe at this, but this greatly simplifies the code generation and the overall mental model when designing your entities. They can cringe more when I say there is no notion of foreign keys in Qleany internal database.

When to use each:

RelationshipUse when…Example
one_to_oneExactly one related entity, exclusiveUser → Profile
many_to_oneMany entities reference one childCar → Brand, Comment → Post
one_to_manyParent owns a collection of childrenBinder → Items, Post → Comments
ordered_one_to_manySame as above, but order mattersBook → Chapters, Playlist → Songs
many_to_manyEntities share references both waysItems ↔ Tags, Students ↔ Courses

There is no ordered_many_to_many because I’m not mad enough to handle that complexity.

Relationship Examples

# Exclusive single reference (0..1) — each side has at most one. UserProfile is owned.
- name: profile
  type: entity
  entity: UserProfile
  relationship: one_to_one
  strong: true
  optional: true

# Back-reference to parent (N:1) — many children point to one parent
- name: parentItem
  type: entity
  entity: BinderItem
  relationship: many_to_one

# Required back-reference
- name: binder
  type: entity
  entity: Binder
  relationship: many_to_one
  # implied:
  optional: false

# Unordered children with cascade delete (1:N)
- name: tags
  type: entity
  entity: BinderTag
  relationship: one_to_many
  strong: true

# Ordered children (1:N with order)
- name: chapters
  type: entity
  entity: BinderItem
  relationship: ordered_one_to_many
  strong: true

# Shared references (N:M)
- name: tags
  type: entity
  entity: BinderTag
  relationship: many_to_many

Weak Relationships

Both many_to_one and many_to_many are always weak — they reference entities owned elsewhere. They cannot have strong: true. In Qleany, the owning side (with a strong relationship) controls cascade deletion. For many_to_one and many_to_many, theorically, it would mean that a child would be deleted only if there was no parent left to “own” it. Too difficult to implement, so no.

Dev note: theoretically, you can play with the junction table code base to support many_to_one with strong ownership, but that would be a nightmare to maintain and reason about.

entities:
  - name: Work
    fields:
      - name: tags                        # Owns the tags (strong one-to-many)
        type: entity
        entity: BinderTag
        relationship: one_to_many
        strong: true

  - name: Binder
    fields:
      - name: items                       # Owns the items (strong ordered)
        type: entity
        entity: BinderItem
        relationship: ordered_one_to_many
        strong: true

  - name: BinderItem
    fields:
      - name: binder                      # Back-reference (weak many-to-one)
        type: entity
        entity: Binder
        relationship: many_to_one

      - name: tags                        # Shared reference (weak many-to-many)
        type: entity
        entity: BinderTag
        relationship: many_to_many

Rule of thumb: Every entity referenced by a weak relationship (many_to_one or many_to_many) must be strongly owned somewhere else in your entity graph. Without strong ownership, entities become orphans with no lifecycle management.


Features and Use Cases

Features group related use cases together.

features:
  - name: file_management
    use_cases:
      - name: load_file
        # ...
      - name: save_file
        # ...

Use Case Options

OptionTypeDefaultDescription
namestringrequiredUse case name (snake_case)
undoableboolfalseGenerate undo/redo command scaffolding
read_onlyboolfalseNo data modification (affects generated code)
long_operationboolfalseAsync execution with progress (Rust only)
entitieslist[]Entities this use case works with
dto_inobjectnoneInput DTO for this use case
dto_outobjectnoneOutput DTO for this use case

In Rust, entities are doing a bit of the legwork for you to define which repositories are injected into the use case struct and prepare the use of a special macro macros::uow_action to simplify unit of work handling. These macro lines must be adapted in your use cases files, and the exact same macros must be repeated in these use cases’ unit of work files. Commentary lines will be generated to help you find and adapt these lines.

Similar macros are offered on the C++/Qt side.

DTOs

Each use case can have input and output DTOs, or only one, or none at all.

use_cases:
  - name: import_inventory
    dto_in:
      name: ImportInventoryDto
      fields:
        - name: file_path
          type: string
        - name: skip_header
          type: boolean
        - name: inventory_type
          type: enum
          enum_name: InventoryType
          enum_values:
            - Full
            - Incremental
    dto_out:
      name: ImportReturnDto
      fields:
        - name: imported_count
          type: integer
        - name: error_messages
          type: string
          is_list: true

You can’t put entities in DTOs. Only primitive types are allowed because entities are tied to the database and business logic, while DTOs are simple data carriers. Think of it as a way to control the data flow between the UI and the business logic. You don’t want to expose a password to the user UI just because it’s in an entity.

DTO Field Options

OptionTypeDefaultDescription
namestringrequiredField name (snake_case)
typestringrequiredField type (boolean, integer, float, string, uuid, datetime)
is_listboolfalseField is a list/array
optionalboolfalseField can be Option<>/std::optional
enum_namestringnoneFor enum type, name of the enum
enum_valueslistnoneFor enum type, list of possible values

User Interface Options


  ui:
    rust_cli: true
    rust_slint: true
    cpp_qt_qtwidgets: false
    cpp_qt_qtquick: false


These options allow the generation of scaffolding for the UI. They will each live in their own separate folders, also separate from the common backend code.

One backend, many frontends.

Generated Infrastructure - Rust

This document details the infrastructure Qleany generates for Rust. It’s a reference material — read it when you need to understand, extend, or debug the generated code, not as a getting-started guide.

Rust Infrastructure

redb Backend

Embedded key-value storage with ACID transactions. Qleany generates a trait-based abstraction layer:

#![allow(unused)]
fn main() {
// Table trait (generated) — implemented by redb storage
pub trait WorkspaceTable {
    fn create(&mut self, entity: &Workspace) -> Result<Workspace, Error>;
    fn create_multi(&mut self, entities: &[Workspace]) -> Result<Vec<Workspace>, Error>;
    fn get(&self, id: &EntityId) -> Result<Option<Workspace>, Error>;
    fn get_multi(&self, ids: &[EntityId]) -> Result<Vec<Option<Workspace>>, Error>;
    fn update(&mut self, entity: &Workspace) -> Result<Workspace, Error>;
    fn update_multi(&mut self, entities: &[Workspace]) -> Result<Vec<Workspace>, Error>;
    fn delete(&mut self, id: &EntityId) -> Result<(), Error>;
    fn delete_multi(&mut self, ids: &[EntityId]) -> Result<(), Error>;

    fn get_relationship(
        &self,
        id: &EntityId,
        field: &WorkspaceRelationshipField,
    ) -> Result<Vec<EntityId>, Error>;
    fn get_relationships_from_right_ids(
        &self,
        field: &WorkspaceRelationshipField,
        right_ids: &[EntityId],
    ) -> Result<Vec<(EntityId, Vec<EntityId>)>, Error>;
    fn set_relationship_multi(
        &mut self,
        field: &WorkspaceRelationshipField,
        relationships: Vec<(EntityId, Vec<EntityId>)>,
    ) -> Result<(), Error>;
    fn set_relationship(
        &mut self,
        id: &EntityId,
        field: &WorkspaceRelationshipField,
        right_ids: &[EntityId],
    ) -> Result<(), Error>;
}

// Repository wraps table with event emission
pub struct WorkspaceRepository<'a> {
    redb_table: Box<dyn WorkspaceTable + 'a>,
    transaction: &'a Transaction,
}
}

Read-only operations use a separate WorkspaceTableRO trait and WorkspaceRepositoryRO struct, enforcing immutability at the type level.

Long Operation Manager

Threaded execution for heavy tasks:

#![allow(unused)]
fn main() {
pub fn generate_rust_files(
    db_context: &DbContext,
    event_hub: &Arc<EventHub>,
    long_operation_manager: &mut LongOperationManager,
    dto: &GenerateRustFilesDto,
) -> Result<String> {
    let uow_context = GenerateRustFilesUnitOfWorkFactory::new(&db_context);
    let uc = GenerateRustFilesUseCase::new(Box::new(uow_context), dto);
    let operation_id = long_operation_manager.start_operation(uc);
    Ok(operation_id)
}

pub fn get_generate_rust_files_progress(
    long_operation_manager: &LongOperationManager,
    operation_id: &str,
) -> Option<OperationProgress> {
    long_operation_manager.get_operation_progress(operation_id)
}

pub fn get_generate_rust_files_result(
    long_operation_manager: &LongOperationManager,
    operation_id: &str,
) -> Result<Option<GenerateRustFilesReturnDto>> {
    // Get the operation result as a JSON string
    let result_json = long_operation_manager.get_operation_result(operation_id);

    // If there's no result, return None
    if result_json.is_none() {
        return Ok(None);
    }

    // Parse the JSON string into a GenerateRustFilesResultDto
    let result_dto: GenerateRustFilesReturnDto = serde_json::from_str(&result_json.unwrap())?;

    Ok(Some(result_dto))
}
}

Features:

  • Progress callbacks with percentage and message
  • Cancellation support
  • Result or error on completion

Ephemeral Database Pattern

The internal database lives in memory, decoupled from user files:

  1. Load: Transform file → internal database
  2. Work: All operations against ephemeral database
  3. Save: Transform internal database → file

This pattern separates the user’s file format from internal data structures. Your .myapp file can be JSON, XML, SQLite, or any format. The internal database remains consistent.

The user must implement this pattern in dedicated custom use cases.

Synchronous Undo/Redo Commands

Rust uses synchronous command execution (unlike C++/Qt’s async controller layer). Each use case implements UndoRedoCommand and maintains its own undo/redo stacks using VecDeque:

#![allow(unused)]
fn main() {
pub struct UpdateWorkspaceUseCase {
    uow_factory: Box<dyn WorkspaceUnitOfWorkFactoryTrait>,
    undo_stack: VecDeque<Workspace>,
    redo_stack: VecDeque<Workspace>,
}

impl UndoRedoCommand for UpdateWorkspaceUseCase {
    fn undo(&mut self) -> Result<()> {
        if let Some(last_entity) = self.undo_stack.pop_back() {
            let mut uow = self.uow_factory.create();
            uow.begin_transaction()?;
            uow.update_workspace(&last_entity)?;
            uow.commit()?;
            self.redo_stack.push_back(last_entity);
        }
        Ok(())
    }

    fn redo(&mut self) -> Result<()> {
        if let Some(entity) = self.redo_stack.pop_back() {
            let mut uow = self.uow_factory.create();
            uow.begin_transaction()?;
            uow.update_workspace(&entity)?;
            uow.commit()?;
            self.undo_stack.push_back(entity);
        }
        Ok(())
    }
}
}

Controllers manage the UndoRedoManager and optional scoped stacks:

#![allow(unused)]
fn main() {
pub fn update(
    db_context: &DbContext,
    event_hub: &Arc<EventHub>,
    undo_redo_manager: &mut UndoRedoManager,
    stack_id: Option<u64>,
    entity: &WorkspaceDto,
) -> Result<WorkspaceDto> {
    let uow_factory = WorkspaceUnitOfWorkFactory::new(&db_context, &event_hub);
    let mut uc = UpdateWorkspaceUseCase::new(Box::new(uow_factory));
    let result = uc.execute(entity)?;
    undo_redo_manager.add_command_to_stack(Box::new(uc), stack_id)?;
    Ok(result)
}
}

Unlike C++/Qt’s async controller layer, Rust uses fully synchronous execution throughout, which works well for CLI where blocking is acceptable. I choose to avoid async/await complexity here.

Event Hub

Channel-based event dispatch using a unified Event struct:

#![allow(unused)]
fn main() {
// Event structure (generated)
pub struct Event {
    pub origin: Origin,
    pub ids: Vec<EntityId>,
    pub data: Option<String>,
}

pub enum Origin {
    DirectAccess(DirectAccessEntity),
    Feature(FeatureEntity),
}

pub enum DirectAccessEntity {
    Workspace(EntityEvent),
    Entity(EntityEvent),
    // ... other entities
}

pub enum EntityEvent {
    Created,
    Updated,
    Removed,
}
...

// Publishing (from the repositories)
event_hub.send_event(Event {
    origin: Origin::DirectAccess(DirectAccessEntity::Workspace(EntityEvent::Updated)),
    ids: vec![entity.id.clone()],
    data: None,
});
}

Repository

Both languages generate repositories with batch-capable interfaces:

MethodPurpose
create(entity) / create_multi(entities)Insert new entities
get(id) / get_multi(ids)Fetch entities
update(entity) / update_multi(entities)Update existing entities
delete(id) / delete_multi(ids)Delete entities (cascade for strong relationships)

Relationship-specific methods:

MethodPurpose
get_relationship(id, field)Get related IDs for one entity
get_relationships_from_right_ids(field, ids)Reverse lookup
set_relationship(id, field, ids)Set relationship for one entity
set_relationship_multi(field, relationships)Batch relationship updates

Unit of Work

In Rust, the units of work are helped by macros to generate all the boilerplate for transaction management and repository access. This can be a debatable design choice, since all is already generated by Qleany. The reality is : not all can be generated. The user (developer) has the responsibility to adapt the units of work for each custom use case. The macros are here to ease this task.

I repeat: the user is to adapt the macros in custom use cases.

Each use case receives a unit of work factory which handles the unit of work creation that allow transaction-scoped operations:

#![allow(unused)]
fn main() {
// In the controller, we create the use case with a factory for the unit of work

pub fn create(
    db_context: &DbContext,
    event_hub: &Arc<EventHub>,
    undo_redo_manager: &mut UndoRedoManager,
    stack_id: Option<u64>,
    entity: &CreateWorkspaceDto,
) -> Result<WorkspaceDto> {
    let uow_factory = WorkspaceUnitOfWorkFactory::new(db_context, event_hub);
    let mut uc = CreateWorkspaceUseCase::new(Box::new(uow_factory));
    let result = uc.execute(entity.clone())?;
    undo_redo_manager.add_command_to_stack(Box::new(uc), stack_id)?;
    Ok(result)
}

// In the unit of work, you see a bit of macro magic to generate all the boilerplate:

#[macros::uow_action(entity = "Workspace", action = "Create")]
#[macros::uow_action(entity = "Workspace", action = "CreateMulti")]
#[macros::uow_action(entity = "Workspace", action = "Get")]
#[macros::uow_action(entity = "Workspace", action = "GetMulti")]
#[macros::uow_action(entity = "Workspace", action = "Update")]
#[macros::uow_action(entity = "Workspace", action = "UpdateMulti")]
#[macros::uow_action(entity = "Workspace", action = "Delete")]
#[macros::uow_action(entity = "Workspace", action = "DeleteMulti")]
#[macros::uow_action(entity = "Workspace", action = "GetRelationship")]
#[macros::uow_action(entity = "Workspace", action = "GetRelationshipsFromRightIds")]
#[macros::uow_action(entity = "Workspace", action = "SetRelationship")]
#[macros::uow_action(entity = "Workspace", action = "SetRelationshipMulti")]
impl WorkspaceUnitOfWorkTrait for WorkspaceUnitOfWork {}
}

DTO Mapping

DTOs are generated for all boundary crossings:

Controller ←→ CreateCarDto ←→ UseCase ←→ Car (Entity) ←→ Repository

The separation ensures:

  • Controllers don’t expose entity internals
  • You control what data flows in/out of each layer

File Organization

Cargo.toml
crates/
├── cli/
│   ├── src/
│   │   ├── main.rs    
│   └── Cargo.toml
├── common/
│   ├── src/
│   │   ├── entities.rs             # Generated entities
│   │   ├── database.rs
│   │   ├── database/
│   │   │   ├── db_context.rs
│   │   │   ├── db_helpers.rs
│   │   │   └── transactions.rs
│   │   ├── direct_access.rs
│   │   ├── direct_access/         # Holds the repository and table implementations for each entity
│   │   │   ├── car.rs
│   │   │   ├── car/
│   │   │   │   ├── car_repository.rs
│   │   │   │   └── car_table.rs
│   │   │   ├── customer.rs
│   │   │   ├── customer/
│   │   │   │   ├── customer_repository.rs
│   │   │   │   └── customer_table.rs
│   │   │   ├── sale.rs
│   │   │   ├── sale/
│   │   │   │   ├── sale_repository.rs
│   │   │   │   └── sale_table.rs
│   │   │   ├── root.rs
│   │   │   ├── root/
│   │   │   │   ├── root_repository.rs
│   │   │   │   └── root_table.rs
│   │   │   ├── repository_factory.rs
│   │   │   └── setup.rs
│   │   ├── event.rs             # event system for reactive updates
│   │   ├── lib.rs
│   │   ├── long_operation.rs    # infrastructure for long operations
│   │   ├── types.rs         
│   │   └── undo_redo.rs        # undo/redo infrastructure
│   └── Cargo.toml
├── direct_access/                   # a direct access point for UI or CLI to interact with entities
│   ├── src/
│   │   ├── car.rs
│   │   ├── car/
│   │   │   ├── car_controller.rs   # Exposes CRUD operations to UI or CLI
│   │   │   ├── dtos.rs
│   │   │   ├── units_of_work.rs
│   │   │   ├── use_cases.rs
│   │   │   └── use_cases/          # The logic here is auto-generated
│   │   │       ├── create_car_uc.rs
│   │   │       ├── get_car_uc.rs
│   │   │       ├── update_car_uc.rs
│   │   │       ├── remove_car_uc.rs
│   │   │       └── ...
│   │   ├── customer.rs
│   │   ├── customer/
│   │   │   └── ...
│   │   ├── sale.rs
│   │   ├── sale/
│   │   │   └── ...
│   │   ├── root.rs
│   │   ├── root/
│   │   │   └── ...
│   │   └── lib.rs
│   └── Cargo.toml
└── inventory_management/
    ├── src/
    │   ├── inventory_management_controller.rs
    │   ├── dtos.rs
    │   ├── units_of_work.rs
    │   ├── units_of_work/          # ← adapt the macros here
    │   │   └── ...
    │   ├── use_cases.rs
    │   ├── use_cases/              # ← You implement the logic here
    │   │   └── ...
    │   └── lib.rs
    └── Cargo.toml

Generated Infrastructure - C++/Qt

This document details the infrastructure Qleany generates for C++20/Qt6. It’s a reference material — read it when you need to understand, extend, or debug the generated code, not as a getting-started guide.

C++/Qt Infrastructure

Database Layer

DbContext / DbSubContext: Connection pool with scoped transactions. Each unit of work owns a DbSubContext providing beginTransaction, commit, rollback, and savepoint support. Savepoint is present just in case the developer really needs it, Qleany doesn’t use it internally (see the undo redo documentation if you want to know why)

Repository Factory: Creates repositories bound to a specific DbSubContext and EventRegistry. Returns owned instances (std::unique_ptr) — no cross-thread sharing. Every command/query holds its own DbSubContext, table, and repository instances.

auto repo = RepositoryFactory::createWorkRepository(m_dbSubContext, m_eventRegistry);
auto works = repo->get(QList<int>{workId});

Table Cache / Junction Cache: Thread-safe, time-expiring (30 minutes), invalidated at write time. Improves performance for repeated queries within a session.

SQLite Configuration

SQLite with WAL mode, optimized for desktop applications:

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA cache_size=20000;        -- 20MB
PRAGMA mmap_size=268435456;     -- 256MB

These are tuned for a typical desktop workload: frequent reads with occasional write bursts, and a single user.

WAL mode lets reads and writes happen concurrently instead of blocking each other. This keeps the UI responsive while background operations write to the database. The tradeoff is two sidecar files (-wal, -shm) next to the database, harmless for a local application.

NORMAL synchronous skips fsync() on every commit. A power failure could lose the last transaction, but the database won’t corrupt. For a desktop app where users already expect to lose unsaved work after a crash, this is a good trade for faster writes.

20 MB page cache (~10× the default) keeps hot pages in memory so navigation stays snappy. Negligible cost on any modern machine.

256 MB memory-map lets the OS map the database file into the process address space, bypassing SQLite’s I/O layer for reads. Since most desktop databases stay well under this limit, the entire file benefits. Works well on Linux and macOS; functional on Windows, though less battle-tested there.

Together, these give fast, responsive data access while accepting a durability tradeoff that simply doesn’t matter for a single-user desktop application.

Ephemeral Database Pattern

The internal database lives in /tmp/, decoupled from user files:

  1. Load: Transform file → internal database
  2. Work: All operations against ephemeral database
  3. Save: Transform internal database → file

This pattern separates the user’s file format from internal data structures. Your .myapp file can be JSON, XML, SQLite, or any format. The internal database remains consistent.

The user must implement this pattern in dedicated custom use cases.

Note: the SQLite database can be set to exist in memory by using :memory: as the filename.

Async Undo/Redo with QCoro

Controllers use C++20 coroutines via QCoro for non-blocking command execution:

QCoro::Task<QList<WorkDto>> WorkController::update(const QList<WorkDto> &works, int stackId)
{
    if (!m_undoRedoSystem)
    {
        qCritical() << "UndoRedo system not available";
        co_return QList<WorkDto>();
    }

    // Create use case that will be owned by the command
    std::unique_ptr<IWorkUnitOfWork> uow = std::make_unique<WorkUnitOfWork>(*m_dbContext, m_eventRegistry);
    auto useCase = std::make_shared<UpdateWorkUseCase>(std::move(uow));

    // Create command that owns the use case
    auto command = std::make_shared<Common::UndoRedo::UndoRedoCommand>("Update Works Command"_L1);
    QList<WorkDto> result;

    // Create weak_ptr to break circular reference
    std::weak_ptr<UpdateWorkUseCase> weakUseCase = useCase;

    // Prepare lambda for execute - use weak_ptr to avoid circular reference
    command->setExecuteFunction([weakUseCase, works, &result](auto &) {
        if (auto useCase = weakUseCase.lock())
        {
            result = useCase->execute(works);
        }
    });

    // Prepare lambda for redo - use weak_ptr to avoid circular reference
    command->setRedoFunction([weakUseCase]() -> Common::UndoRedo::Result<void> {
        if (auto useCase = weakUseCase.lock())
        {
            return useCase->redo();
        }
        return Common::UndoRedo::Result<void>("UseCase no longer available"_L1,
                                              Common::UndoRedo::ErrorCategory::ExecutionError);
    });

    // Prepare lambda for undo - use weak_ptr to avoid circular reference
    command->setUndoFunction([weakUseCase]() -> Common::UndoRedo::Result<void> {
        if (auto useCase = weakUseCase.lock())
        {
            return useCase->undo();
        }
        return Common::UndoRedo::Result<void>("UseCase no longer available"_L1,
                                              Common::UndoRedo::ErrorCategory::ExecutionError);
    });

    // Store the useCase in the command to maintain ownership
    // This ensures the useCase stays alive as long as the command exists
    command->setProperty("useCase", QVariant::fromValue(useCase));

    // Execute command asynchronously using QCoro integration
    std::optional<bool> success = co_await m_undoRedoSystem->executeCommandAsync(command, 500, stackId);

    if (!success.has_value())
    {
        qWarning() << "Update work command execution timed out";
        co_return QList<WorkDto>();
    }

    if (!success.value())
    {
        qWarning() << "Failed to execute update work command";
        co_return QList<WorkDto>();
    }

    co_return result;
}

What was written above is the “flattened” version of the real code. The actual code below uses helper functions to make it more readable and less repetitive (in the file common/controller_command_helpers.h). Thus, this same code can be shortened:


QCoro::Task<QList<WorkDto>> WorkController::update(const QList<WorkDto> &files, int stackId)
{
    auto uow = std::make_unique<WorkUnitOfWork>(*m_dbContext, m_eventRegistry);
    auto useCase = std::make_shared<UpdateWorkUseCase>(std::move(uow));

    co_return co_await Helpers::executeUndoableCommand<QList<WorkDto>>(
        m_undoRedoSystem, u"Update works Command"_s, std::move(useCase), stackId, kDefaultCommandTimeoutMs, files);
}

Use cases contain synchronous business logic with state for undo/redo:

QList<WorkDto> UpdateWorkUseCase::execute(const QList<WorkDto> &works)
{
    // Store original state for undo
    m_uow->beginTransaction();
    m_originalWorks = DtoMapper::toDtoList(m_uow->getWork(workIds));

    // Perform update
    auto updatedEntities = m_uow->updateWork(DtoMapper::toEntityList(works));
    m_uow->commit();

    m_updatedWorks = DtoMapper::toDtoList(updatedEntities);
    return m_updatedWorks;
}

Result<void> UpdateWorkUseCase::undo()
{
    m_uow->beginTransaction();
    m_uow->updateWork(DtoMapper::toEntityList(m_originalWorks));
    m_uow->commit();
    return {};
}

Queries (read-only operations) also execute asynchronously:

QCoro::Task<QList<WorkDto>> WorkController::get(const QList<int> &workIds)
{
    co_return co_await Helpers::executeReadQuery<QList<WorkDto>>(
        m_undoRedoSystem, u"Get works Query"_s, [this, workIds]() -> QList<WorkDto> {
            auto uow = std::make_unique<WorkUnitOfWork>(*m_dbContext, m_eventRegistry);
            auto useCase = std::make_unique<GetWorkUseCase>(std::move(uow));
            return useCase->execute(workIds);
        });
}

Features:

  • Undo stacks (per-document undo)
  • Command grouping (multiple operations as one undo step)
  • Timeout handling for long operations

Event Registry

QObject-based event dispatch for reactive updates. Each entity has its own events class:

class WorkEvents : public QObject
{
    Q_OBJECT
public:
    explicit WorkEvents(QObject *parent = nullptr) : QObject(parent)
    {
        // Register metatypes for cross-thread signal delivery
        qRegisterMetaType<QList<int>>("QList<int>");
        qRegisterMetaType<WorkRelationshipField>("WorkRelationshipField");
    }

public Q_SLOTS:
    // Invoked from any thread via QMetaObject::invokeMethod
    void publishCreated(const QList<int> &ids) { Q_EMIT created(ids); }
    void publishUpdated(const QList<int> &ids) { Q_EMIT updated(ids); }
    void publishRemoved(const QList<int> &ids) { Q_EMIT removed(ids); }
    void publishRelationshipChanged(int workId, WorkRelationshipField relationship, 
                                    const QList<int> &relatedIds)
    { Q_EMIT relationshipChanged(workId, relationship, relatedIds); }

Q_SIGNALS:
    void created(const QList<int> &ids);
    void updated(const QList<int> &ids);
    void removed(const QList<int> &ids);
    void relationshipChanged(int workId, WorkRelationshipField relationship, 
                             const QList<int> &relatedIds);
};

Repositories emit events asynchronously via queued connections to ensure thread safety:

// In repository
void WorkRepository::emitUpdated(const QList<int> &ids) const
{
    if (!m_events || ids.isEmpty())
        return;
    QMetaObject::invokeMethod(m_events, "publishUpdated", 
                              Qt::QueuedConnection, Q_ARG(QList<int>, ids));
}

// Subscribing (C++):
connect(s_serviceLocator->eventRegistry()->workEvents(), &WorkEvents::updated, this, &Whatever::onWorkUpdated);
// Subscribing (QML)
Connections {
    target: EventRegistry.workEvents()
    function onWorkUpdated(ids) { doSomething(ids) }
}

The EventRegistry provides access to all entity events from both C++ and QML.

A similar pattern is used for FeatureEventRegistry in common/features/feature_event_registry.h. Each feature (= group of use cases) has its own events class.

// Subscribing (C++):
connect(s_serviceLocator->eventFeatureRegistry()->handlingAppLifecycleEvents(), &HandlingAppLifecycleEvents::initializeAppSignal, this, &Whatever::onInitializeAppSignal);
// Subscribing (QML)
Connections {
target: EventFeatureRegistry.handlingAppLifecycleEvents()
function onInitializeAppSignal() { doSomething() }
}

Repository

Generated repositories are batch-capable interfaces. One repository for each entity type.

MethodPurpose
create(QList<Entity>)Insert new entities
get(QList<int>)Fetch entities by IDs
update(QList<Entity>)Update existing entities
remove(QList<int>)Delete entities (cascade for strong relationships)

Relationship-specific methods:

MethodPurpose
getRelationshipIds(id, field)Get related IDs for one entity
getRelationshipIdsMany(ids, field)Batch lookup
setRelationshipIds(id, field, ids)Set relationship for one entity
getRelationshipIdsCount(id, field)Count related items
getRelationshipIdsInRange(id, field, offset, limit)Paginated access

C++/Qt offers additional pagination and counting methods for UI scenarios. The generated QAbstractListModels aren’t using these yet but can be extended to do so.

Unit of Work

In C++/Qt, the units of work are helped by macros and inherited classes to generate all the boilerplate for transaction management and repository access. This can be a debatable design choice, since all is already generated by Qleany. The reality is: not all can be generated. The user (developer) has the responsibility to adapt the units of work for each custom use case. The macros are here to ease this task.

I repeat: the user is to adapt the macros in custom use cases.

No factory for the unit of work here, the controller creates a new unit of work per use case.

The examples are from Qleany’s own code. The C++ parts are generated from the same manifest file.

Here is the unit of work used by all CRUD use cases of the Workspace entity. You can see SCUoW::EntityFullImpl inheritance, which refactors all the boilerplate of all entities units of work into a single base template class (in common/unit_of_work/uow_ops.h file).

// Unit of work encapsulates repository access 
class WorkspaceUnitOfWork final
    : public SCUoW::UnitOfWorkBase,
      public SCUoW::EntityFullImpl<WorkspaceUnitOfWork, SCE::Workspace, SCDWorkspace::WorkspaceRelationshipField>
{
    friend class SCUoW::EntityFullImpl<WorkspaceUnitOfWork, SCE::Workspace, SCDWorkspace::WorkspaceRelationshipField>;

  public:
    WorkspaceUnitOfWork(SCDatabase::DbContext &dbContext, const QPointer<SCD::EventRegistry> &eventRegistry)
        : UnitOfWorkBase(dbContext, eventRegistry)
    {
    }
    ~WorkspaceUnitOfWork() override = default;

    /// Called by CRTP base to get a repository for Workspace.
    [[nodiscard]] auto makeRepository()
    {
        return SCD::RepositoryFactory::createWorkspaceRepository(m_dbSubContext, m_eventRegistry);
    }
};

// In controller :

QCoro::Task<QList<WorkDto>> WorkController::create(const QList<CreateWorkDto> &works)
{
...
    auto uow = std::make_unique<WorkUnitOfWork>(*m_dbContext, m_eventRegistry);
    auto useCase = std::make_shared<CreateWorkUseCase>(std::move(uow));
...
}

This keeps transaction boundaries explicit and testable.

For the custom use cases, things are done a bit differently to make adaptation easier. We are using dumb variadic macros here:



class SaveUnitOfWork : public Common::UnitOfWork::UnitOfWorkBase, public ISaveUnitOfWork
{
  public:
    SaveUnitOfWork(SCDatabase::DbContext &db, QPointer<SCD::EventRegistry> eventRegistry,
                   QPointer<SCF::FeatureEventRegistry> featureEventRegistry)
        : UnitOfWorkBase(db, eventRegistry), m_featureEventRegistry(featureEventRegistry)
    {
    }

    /* TODO: adapt entities to real use :
     * Available Atomic Macros (uow_macros.h — for custom UoWs):
     *   Interface:    UOW_ENTITY_{CREATE,GET,UPDATE,REMOVE,CRUD}(Name)
     *                 UOW_ENTITY_RELATIONSHIPS(Name, Rel)
     *
     * The equivalent macros (with the DECLARE_ prefix) must be set in the use case's unit of work interface file
     * in use_cases/i_save_uow.h
     */
    UOW_ENTITY_UPDATE(Dto);
    UOW_ENTITY_UPDATE(DtoField);
    UOW_ENTITY_UPDATE(Global);
    UOW_ENTITY_UPDATE(Relationship);
    UOW_ENTITY_UPDATE(Root);
    UOW_ENTITY_UPDATE(Entity);
    UOW_ENTITY_UPDATE(Field);
    UOW_ENTITY_UPDATE(Feature);
    UOW_ENTITY_UPDATE(UseCase);

    void publishSaveSignal() override;

  private:
    QPointer<SCF::FeatureEventRegistry> m_featureEventRegistry;
};

inline void SaveUnitOfWork::publishSaveSignal()
{
    m_featureEventRegistry->handlingManifestEvents()->publishSaveSignal();
}

The interface in handling_manifest/use_cases/save_uc/i_save_uow.h:


class ISaveUnitOfWork : public virtual Common::UnitOfWork::ITransactional
{
  public:
    ~ISaveUnitOfWork() override = default;

    /* TODO: adapt entities to real use :
     * Available Atomic Macros (uow_macros.h — for custom UoWs):
     *   Interface:    DECLARE_UOW_ENTITY_{CREATE,GET,UPDATE,REMOVE,CRUD}(Name)
     *                 DECLARE_UOW_ENTITY_RELATIONSHIPS(Name, Rel)
     *
     * The equivalent macros (without the DECLARE_ prefix) must be set in the use case's unit of work file
     * in units_of_work/save_uow.h
     */
    DECLARE_UOW_ENTITY_UPDATE(Dto);
    DECLARE_UOW_ENTITY_UPDATE(DtoField);
    DECLARE_UOW_ENTITY_UPDATE(Global);
    DECLARE_UOW_ENTITY_UPDATE(Relationship);
    DECLARE_UOW_ENTITY_UPDATE(Root);
    DECLARE_UOW_ENTITY_UPDATE(Entity);
    DECLARE_UOW_ENTITY_UPDATE(Field);
    DECLARE_UOW_ENTITY_UPDATE(Feature);
    DECLARE_UOW_ENTITY_UPDATE(UseCase);

    virtual void publishSaveSignal() = 0;
};

DTO Mapping

DTOs are generated for all boundary crossings:

Controller ←→ CreateCarDto ←→ UseCase ←→ Car (Entity) ←→ Repository

The separation ensures:

  • Controllers don’t expose entity internals
  • You control what data flows in/out of each layer

The DTOs are C++20 struct aggregates that are enhanced by Q_GADGET macro. They are usable in QML.


File Organization

CMakeLists.txt are disseminated across the project and are not shown here.

src/
├── common/
│   ├── service_locator.h/.cpp
│   ├── database/                           # database infrastructure
│   │   ├── junction_table_ops/...
│   │   ├── db_builder.h
│   │   ├── db_context.h
│   │   └── table_cache.h
│   ├── entities/                      # Generated entities
│   │   ├── my_entity.h
│   │   └── ...
│   ├── unit_of_work/                  # unit of work macros and base class
│   │   ├── unit_of_work.h
│   │   ├── uow_macros.h 
│   │   └── ...
│   ├── features/
│   │   ├── feature_event_registry.h   # Event registry for feature events
│   │   └── ...
│   ├── direct_access/                     # Holds the repositories and tables
│   │   ├── repository_factory.h/.cpp
│   │   ├── event_registry.h
│   │   └── {entity}/
│   │       ├── i_{entity}_repository.h   # Interface with relationship enum
│   │       ├── table_definitions.h       #  Table schema definitions
│   │       ├── {entity}_repository.h/.cpp
│   │       ├── {entity}_table.h/.cpp
│   │       └── {entity}_events.h
│   └── undo_redo/ ...                      # undo/redo infrastructure
├── direct_access/                          # Direct access to entity controllers and use cases
│   └── {entity}/
│       ├── {entity}_controller.h/.cpp
│       ├── dtos.h
│       ├── unit_of_work.h
│       └── use_cases/
└── {feature}/                              # Custom controllers and use cases
    ├── {feature}_controller.h/.cpp
    ├── {feature}_dtos.h
    ├── units_of_work/                    
    │   └── {use case}_uow.h                # ← adapt the macros here
    └── use_cases/              
        ├── {use case}_uc/                  # store here the use case's companion modules
        │   └── i_{use case}_uow.h          # ← adapt the macros here too
        └── {use case}_uc.h/.cpp            # ← You implement the logic here

QML Integration (C++/Qt)

This document covers QML-based frontends: QtQuick. They use QML, so the generated models and patterns apply equally to each.


Qleany generates reactive models ready for QML binding — no manual QAbstractListModel boilerplate.

List Models

{Parent}{Relationship}ListModel provides a standard QAbstractListModel that:

  • Auto-updates when entities change (via EventRegistry subscription)
  • Refreshes only affected rows, not the entire model
  • Supports inline editing through setData with async persistence
  • Exposes all entity fields as roles
ListView {
    model: RootRecentWorksListModel {
        rootId: 1
    }
    delegate: ItemDelegate {
        text: model.title
        subtitle: model.absolutePath
        onClicked: openWork(model.itemId)
    }
}

The model subscribes to two event sources:

  • Entity events (RecentWorkEvents.updated) — refreshes only affected rows
  • Parent events (RootEvents.updated) — full refresh if the relationship changed (items added/removed or reordered)

This means if another part of the application updates a RecentWork’s title, the ListView updates automatically. If the Root’s recentWorks list changes (item added/removed), the model detects the difference and refreshes.

Single Entity Models

Single{Entity} wraps one entity instance for detail views and editor panels. The naming parallels {Entity}ListModel: where list models expose collections, single models expose individual entities. Instead of manually fetching the entity and wiring up change notifications, use Single{Entity}

Single{Entity} wraps one entity with:

  • itemId property to select which entity
  • Auto-fetch on ID change
  • Reactive updates when the entity changes elsewhere in the application
  • All fields are exposed as Q_PROPERTYs with change signals
  • Relationship IDs available for further queries
SingleBinderItem {
    id: currentItem
    itemId: selectedItemId
}

Column {
    Text { text: currentItem.title }
    Text { text: currentItem.subTitle }
    Text { text: "Children: " + currentItem.binderItems.length }
    Text { text: "Parent: " + currentItem.parentItem }
}

The model subscribes to BinderItemEvents.updated — if any part of the application modifies this entity, the properties update automatically and QML bindings refresh.

Note: Since id is a reserved word in QML, the property is named itemId. It corresponds to the entity’s primary key.

Enabling Model Generation

To generate models for an entity, configure these options in the manifest:

At entity level:

- name: Work
  inherits_from: EntityBase
  single_model: true    # Generates SingleWork

At field level (for relationship fields):

fields:
  - name: binders
    type: entity
    entity: Binder
    relationship: ordered_one_to_many
    strong: true
    list_model: true                      # Generates BinderListModelFromWorkBinders
    list_model_displayed_field: name      # Default display role

QML Mocks

Generated JavaScript stubs in mock_imports/ mirror the real C++ API:

mock_imports/
└── Skr/
    ├── Controllers/
    │   ├── qmldir                                      # QML module definition
    │   ├── QCoroQmlTask.qml                            # Mock QCoro integration helper
    │   ├── EventRegistry.qml                           # EventRegistry
    │   ├── RootController.qml                          #
    │   ├── RootEvents.qml                              # Event signals for Root entity
    │   ├── BinderItemController.qml
    │   ├── BinderItemEvents.qml
    │   ├── RecentWorkController.qml
    │   └── RecentWorkEvents.qml
    ├── Models/
    │   ├── qmldir
    │   └── RecentWorkListModelFromRootRecentWorks.qml
    └── Singles/
        ├── qmldir
        └── SingleBinderItem.qml

Build with YOUR_APP_BUILD_WITH_MOCKS to develop UI without backend compilation:

option(YOUR_APP_BUILD_WITH_MOCKS "Build with QML mocks instead of real backend" OFF)

UI developers can iterate on screens with mock data. When ready, disable the flag and the real controllers take over with no QML changes required.

Just to be clear: the mocks are only for UI development. They don’t implement real business logic or data persistence.

Here is the equivalent real C++ import structure:

real_imports/
├── CMakeLists.txt
├── controllers/
│   ├── CMakeLists.txt
│   ├── foreign_root_controller.h
│   ├── foreign_binder_item_controller.h
│   ├── foreign_recent_work_controller.h
│   └── foreign_event_registry.h
├── models/
│   ├── CMakeLists.txt
│   └── foreign_recent_work_list_model_from_root_recent_works.h
└── singles/
    ├── CMakeLists.txt
    └── foreign_single_binder_item.h

Event System

The EventRegistry provides decoupled communication between the backend and QML:

// Generated in common/direct_access/{entity}/{entity}_events.h
class BinderItemEvents : public QObject {
    Q_OBJECT
signals:
    void created(QList<int> ids);
    void updated(QList<int> ids);
    void removed(QList<int> ids);
    void relationshipChanged(int id, BinderItemRelationshipField relationship, const QList<int> &relatedIds)
};

Models automatically subscribe to relevant events. You can also subscribe directly in QML for custom behavior:

Connections {
    target: EventRegistry.binderItem()
    function onCreated(id) {
        console.log("New BinderItem created:", id)
    }
}

To avoid blocking the UI, it’s a common pattern to execute an action from QML, then react to the resulting event. It’s known that the indirection makes debugging difficult and can cause race conditions with multiple subscribers. It’s a mess, so my recommendation is to avoid this antipattern. Instead, let models handle updates reactively when possible.

To access entities directly without going through models, use QCoro to await results from their dedicated entities controllers.

Note: you can’t chain “.then(…)” with QCoro calls directly because they return QCoroQmlTask, not a JavaScript Promise.

There is no model for custom features and their use cases. Like entities, you can access them through their controllers, using QCoro to await results directly instead of relying on events:

import YouApp.Controllers

WorkManagementController {
    id: workManagementController

}

Button {
    id: saveButton

    text: "Save"

    onClicked: {
        console.log("Save button clicked");
        let dto = workManagementController.getSaveWorkDto();
        dto.fileName = "/tmp/mywork.skr";

        workManagementController.saveWork(dto).then(function (result) {
            console.log("Async save result :", result);
        });
    }
}

Best Practices

Prefer list models over manual fetching. The generated models handle caching, updates, and memory management. Fetching entity lists manually and storing them in JavaScript arrays loses reactivity.

Use Single models for detail views. When displaying one entity’s details (an editor panel, a detail page), Single{Entity} gives you reactive properties without you having to manage refresh logic.

Keep model instances alive. Creating a new model instance on every navigation discards cached data and subscriptions. Declare models at component level.

Use QCoro for direct commands. For actions outside of models, like custom features/use cases, use QCoro to await the result instead of relying on events.

Leverage displayed field for simple lists. The list_model_displayed_field provides a sensible default for list delegates. For complex delegates, access individual roles directly.

Troubleshooting

This guide helps you diagnose and fix common issues when working with Qleany.


Generation Issues

Files aren’t being generated where I expect

When you click “Generate” and nothing appears in your project, first check the prefix_path in your manifest’s global section. This path is prepended to all generated file paths. If your manifest says prefix_path: src but your project expects files in source/, the files will be written to the wrong location.

In the UI, is the “in temp/” checkbox selected? If so, files are written to a temporary folder inside the Qleany application data directory, not your project folder. Uncheck it to write directly to your project. For a minimum of safety, this checkbox is checked by default at each launch.

Also, verify you’ve actually selected files to generate. The Generate tab shows all potential files, but only checked files are written. If you accidentally unchecked everything, clicking “Generate (0)” does nothing.

If using the CLI, double-check the --output flag points where you intend. A common mistake is specifying a relative path when you meant absolute, or vice versa.

Some files are missing after generation

Qleany generates files based on what’s defined in your manifest. If an entity is missing its controller and CRUD use cases, check that allow_direct_access isn’t set to false for that entity. Entities with allow_direct_access: false don’t generate files in the direct_access/ folder. This is intentional to hide some entities.

Entity marked by only_for_heritage: true skip all generation. They don’t produce any files and exist solely to be inherited from.

For C++/Qt, if you’re missing list models or single models, verify you’ve enabled list_model: true on the relationship field or single_model: true on the entity itself. These aren’t generated by default.

Generation succeeds but files are empty or truncated

This typically indicates a template error or an edge case in your manifest that the generator didn’t handle. Check that your entity and field names follow the expected conventions (PascalCase for entities, snake_case for fields). Unusual characters or reserved keywords in names can cause template rendering to fail silently.


Compilation Issues

“Module not found” or “File not found” after adding a new entity

When you add a new entity, several aggregate files need to be regenerated together. These files contain references to all entities, so adding one entity means updating them all. If you only regenerated the new entity’s files without updating the aggregate files, the compiler won’t find the new module.

For Rust, regenerate these files together: common/event.rs, common/entities.rs, common/direct_access/repository_factory.rs, common/direct_access/setup.rs, common/direct_access.rs, and direct_access/lib.rs.

For C++/Qt, regenerate: common/database/db_builder.h, common/direct_access/repository_factory.h/.cpp, common/direct_access/event_registry.h, common/direct_access/CMakeLists.txt, common/entities/CMakeLists.txt and direct_access/CMakeLists.txt.

See Regeneration Workflow for the complete list.

Compilation errors after renaming an entity

Qleany doesn’t track renames. If you renamed Car to Vehicle in the manifest and regenerated, you now have both sets of files — the old Car files still exist and may conflict with the new Vehicle files.

Delete the old entity’s files manually. Search your codebase for any remaining references to the old name and update them. Then regenerate the aggregate files mentioned above.

Type mismatch errors in generated code

This usually happens when your manifest has inconsistent relationship definitions. For example, if Entity A declares a one_to_many relationship to Entity B, but Entity B doesn’t exist, the generated code may have type mismatches.

Review your relationship definitions. Every many_to_one or many_to_many relationship should reference an entity that exists and is owned by another entity.

Enums names and entities names must not conflict. If you have an enum named Status and an entity named Status, the generated code will have type conflicts. Rename one of them.


Undo/Redo Issues

Undo affects unexpected entities

This is the classic symptom of violating the undo-redo inheritance rules. If an undoable parent owns a non-undoable child through a strong relationship, undoing the parent can cascade to the child unexpectedly.

Check your entity tree. Every entity under an undoable parent (via strong relationships) must also be undoable. If you have data that shouldn’t participate in undo (settings, caches, temporary state), place it in a separate non-undoable trunk — don’t nest it under undoable entities.

The GUI can generate a Mermaid diagram of your entity tree. This can help visualize the relationships and ensure they align with your undo/redo requirements.

See Undo-Redo Architecture for the complete rules and recommended configurations.

Undo/redo does nothing

First, verify the entity’s controller was generated with undo support. Check that undoable: true is set on the entity in your manifest. If it’s false or omitted, the generated controller won’t include undo/redo infrastructure.

For custom use cases, check that undoable: true is set on the use case definition in the features section. Non-undoable use cases don’t generate command scaffolding.

If undo is enabled but still doesn’t work, verify the undo system is properly initialized. In C++/Qt, the UndoRedoSystem must be instantiated at startup and given to ServiceLocator before any undoable operations. In Rust, it’s similar and depends on each UI, but the instance must be created and passed to controllers.

Redo recreates entities with wrong IDs

This can happen if your undo implementation doesn’t properly store and restore entity IDs. The generated entity’s CRUD use cases store the created ID during execute() and uses it during undo(). If you’ve modified the generated command code, ensure you’re preserving IDs correctly.

In custom feature/use cases, check the execute() and undo() methods to ensure IDs are being preserved.


Database Issues

“Database locked” errors (C++/Qt SQLite)

SQLite allows only one writer at a time. If you see “database is locked” errors, you likely have multiple threads trying to write simultaneously, or a long-running read transaction is blocking writes.

Use the generated DbSubContext for all database operations — it manages transactions properly. Don’t hold transactions open longer than necessary. For long operations, consider breaking them into smaller transactions.

The generated code uses WAL mode, which improves concurrency, but writers still block each other. If you need truly concurrent writes, you may need to queue operations through a single writer thread.

Data doesn’t persist between sessions

Remember that Qleany’s internal database is ephemeral by design. It lives in a temporary location and serves as a working cache, not permanent storage. Your application is responsible for saving data to a user file (JSON, XML, your own SQLite file, etc.) and loading it back.

If you expected the internal database to persist, review the Generated Infrastructure - C++/Qt documentation. The pattern is: load file → populate internal database → work → save to file.

Cache returns stale data

The generated table caches expire after 30 minutes and invalidate on writes. If you’re seeing stale data, check whether writes are going through the proper repository methods. Direct SQL queries that bypass the repository won’t invalidate the cache.

Also, verify you’re not holding onto old entity instances after they’ve been updated. Fetch fresh data from the repository after any operation that might have modified it. Use events/signals to notify UI components when data changes.


QML Integration Issues (C++/Qt)

List model doesn’t update when entities change

The generated list models subscribe to entity events through the EventRegistry. If updates aren’t appearing, verify that the EventRegistry signals are being emitted. Check that the entity’s repository calls emit m_events->updated(id) after modifications.

If events are firing but the model doesn’t update, ensure the model instance is still alive. Creating a new model instance on each navigation might discard the old subscriptions. Declare models at the component level, not inside functions.

Single model shows stale data

Single{Entity} fetches data when itemId changes. If you set the same ID again, it won’t refetch. To force a refresh after knowing the entity changed, you can toggle itemId to -1 and back, or connect directly to the entity’s updated signal and call a refresh method.

QML mocks work but real backend doesn’t

The mocks are JavaScript stubs that return fake data immediately. The real backend is asynchronous and may have different timing. If your QML code assumes synchronous data availability, it will work with mocks but fail with the real backend.

Also check that you’ve correctly switched build configurations. The YOUR_APP_BUILD_WITH_MOCKS CMake option controls whether mocks or real implementations are used. Ensure it’s set to OFF for production builds.


Rust-Specific Issues

“Cannot borrow as mutable” in generated code

The generated Rust code uses specific borrowing patterns. If you’ve modified the generated code and introduced borrow checker errors, review whether you’re trying to mutate while borrowing. Nothing new here.

The generated repositories take &mut self for write operations and &self for reads. Make sure you’re not trying to read and write simultaneously through the same repository instance. The architecture uses the Rust type system to enforce the difference between read and write operations.

Long operation never completes

If a long operation hangs, check that you’re properly awaiting or polling the handle. The LongOperationManager spawns work on a separate thread, but you must check for completion.

Also, verify the operation itself doesn’t have an infinite loop. Add progress reporting (progress.set_percent()) at key points so you can see where it stalls.

Events not received

The Rust event system uses channels. If you’re not receiving events, verify you’ve subscribed before the events were published. Events published before subscription are lost.

Also check that you’re actually polling the receiver. In synchronous code, call rx.recv() or rx.try_recv(). If using async, ensure the receiver task is being polled. Check crates/slint_ui/src/event_hub_client.rs of Qleany repository for an example.


Manifest Issues

“Invalid manifest” error on load

The manifest parser is strict about YAML syntax. Common issues include incorrect indentation (YAML uses spaces, not tabs), missing colons after keys, or unquoted strings that look like other YAML types.

Validate your YAML syntax with an online validator or yamllint before loading in Qleany. The error message should indicate the line number where parsing failed.

Relationship validation fails

Certain relationship configurations are invalid and rejected at parse time. For example, strong: true is only valid on one_to_one, one_to_many, and ordered_one_to_many relationships — not on many_to_one or many_to_many. The optional flag only applies to one_to_one and many_to_one.

See the validation rules table in Manifest Reference.

Changes to manifest don’t appear after regeneration

You may have generated in the temp folder. There is a checkbox in the UI to toggle between temp and project folders.


Getting Help

If you’ve worked through this guide and still have issues, open an issue on GitHub: github.com/jacquetc/qleany/issues

Include in your issue:

  • What you were trying to do
  • What you expected to happen
  • What actually happened
  • Relevant portions of your manifest (sanitize any private information)
  • Error messages or unexpected output
  • Qleany version and target language (Rust or C++/Qt)

Response times may vary, but thoughtful, well-documented issues are more likely to get attention. If you’re a FernTech customer, please contact support for priority assistance.

Contributing to Qleany

Thank you for your interest in contributing to Qleany! This document provides guidelines and information for contributors.

Code of Conduct

Please be respectful and constructive in all interactions. We aim to maintain a welcoming environment for everyone.

How to Contribute

Reporting Issues

  • Check existing issues before creating a new one
  • Provide a clear description of the problem
  • Include steps to reproduce, expected behavior, and actual behavior
  • Mention your environment (OS, Rust version, etc.)

Suggesting Features

  • Open an issue describing the feature and its use case
  • Explain why this would be valuable for Qleany users
  • Be open to discussion about alternative approaches

Submitting Code

  1. Fork the repository
  2. Create a feature branch from main
  3. Make your changes
  4. Ensure your code follows the project’s style
  5. Test your changes
  6. Submit a pull request

Developer Certificate of Origin

This project uses the Developer Certificate of Origin (DCO).

By contributing to this repository, you agree to the DCO. You must sign off your commits to indicate your agreement:

git commit -s -m "Your commit message"

This adds a Signed-off-by: Your Name <your.email@example.com> line to your commit, certifying that you wrote or have the right to submit the code under the project’s license (MPL-2.0).

Setting up automatic sign-off

You can configure Git to always set your identity for your commits for this repository:

git config user.name "Your Name"
git config user.email "your.email@example.com"

Then use git commit -s for each commit, or create a Git alias:

git config --global alias.cs "commit -s"

What if I forgot to sign off?

You can amend your last commit:

git commit --amend -s

For multiple commits, you may need to rebase:

git rebase --signoff HEAD~N

(Replace N with the number of commits to sign off)

License

By contributing to Qleany, you agree that your contributions will be licensed under the Mozilla Public License 2.0.

Questions?

If you have questions about contributing, feel free to open an issue for discussion.

Developer Certificate of Origin

Developer Certificate of Origin Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.

Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.

Developer’s Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or

(b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or

(c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it.

(d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved.