TeaQL Showcase: See What Your Business Code Actually Does
Instead of hiding database behavior behind an opaque ORM, this demo shows the full execution path of a domain action:
Command → Domain transition → SQL → Audit diff → Event log → UI projection

The task board intentionally uses a tiny domain model so the runtime behavior is easy to follow. TeaQL itself is designed for significantly larger business domains, where understanding domain transitions, generated SQL, audit trails and query execution paths becomes even more important.
To make the idea concrete, we built a terminal-based Kanban board using Ratatui + SQLite. When you move a task from Planned to Ready, TeaQL shows the generated SQL, optimistic concurrency update, audit trail, lifecycle event, and refreshed status facets — all in real time.
The app also cross-compiles as a standalone statically linked binary for armv7 router environments, with no external runtime dependencies.
✨ Powered by native rusqlite: The TeaQL code generator natively supports rusqlite, producing 100% Rust-native SQLite execution code that compiles directly into your binary with zero external driver overhead.
TeaQL for Rust
TeaQL is coming to Rust.
We are preparing an open-source Rust-based version of TeaQL, starting with a lightweight generator and runtime foundation for the Rust ecosystem. Depending on when you read this, you may already see the very early teaql/teaql-forge-rs repository, but it should be treated as exploratory work rather than a polished generator/runtime that can run this demo end to end by itself.
The project is still in its early stage. Our goal is to explore how TeaQL's domain query model can work naturally in Rust, and how it can help developers build domain-driven business applications with clearer models, safer queries, and better local tooling.
The early open-source line will focus on:
- a lightweight TeaQL generator for Rust
- basic domain model definitions
- query model and request structures
- runtime foundation for local execution
- simple examples and demo applications
- a developer-friendly project structure for the Rust ecosystem
This project is not intended to be a large framework from day one. We want to start small, make the basic ideas clear, and let developers understand how TeaQL can fit into Rust projects naturally. The early version will focus on clarity, simplicity, and practical usage.
Rust is a good fit for TeaQL's next step because it provides strong type safety, high performance, local-first deployment, single-binary distribution, good support for CLI and developer tools, and a growing ecosystem for business and infrastructure software.
TeaQL for Rust is an open-source Rust-based direction for TeaQL, starting with a lightweight generator and runtime foundation for building domain-driven business applications.
For this task board demo, the TeaQL runtime crates are open source and the generated Rust code is checked into the repository. The production generator currently used to produce that code is still closed-source while we refine and extract a smaller Rust-focused open-source generator. Early open-source generator code in teaql/teaql-forge-rs may not yet be able to reproduce this demo.
📁 Project Structure
robot-task-board/
├── src/
│ ├── app.rs # Core Application State
│ ├── commands.rs # User command parsing (`/add`, `/mv`, etc)
│ ├── logging.rs # TeaQL Audit Sink & Logging extensions
│ ├── main.rs # Event loop & application entry point
│ ├── models.rs # Lightweight models & DTOs for the UI
│ ├── service.rs # Domain behavior, Aggregate Roots & TeaQL queries
│ ├── startup.rs # Animated startup / bootstrap rendering
│ ├── tui.rs # Terminal initialization and restoration
│ ├── ui.rs # Ratatui layout, syntax-highlighted log rendering
│ └── utils.rs # System info (CPU/memory) from /proc
├── models/
│ └── model.xml # Generated by AI via teaql-agent-kit (https://github.com/teaql/teaql-agent-kit), validated & auto-healed via the `teaql eval` command
├── generate-lib/
│ └── lib/ # Auto-generated TeaQL domain library (Generated via `teaql gen-lib models/model.xml`)
├── Cargo.toml
└── README.md
🔬 Why TeaQL? (10 Applied Scenarios)
This application exercises 10 distinct TeaQL capabilities across its CRUD and query workflows. Each scenario below maps a TeaQL API to its concrete usage in this app and the exact SQL it produces.
Scenario 1: Schema Bootstrap (ensure_rusqlite_schema_for)
What it does: Automatically creates or migrates all database tables and seeds initial reference data (status values, platform) from the domain model — zero manual SQL.
// service.rs — One-line schema setup
let mut ctx = robot_kanban::module_with_behaviors_and_checkers().into_context();
ctx.use_rusqlite_provider(inner_executor.clone());
ensure_rusqlite_schema_for(&ctx)?;
Applied in: TaskService::new() — on first run, creates task_data, task_status_data, and platform_data tables with seed data; on subsequent runs, applies any schema changes from the model.
Bonus (Sample Data): Beyond schema creation, the framework also auto-generates a
sample_datamodule from your model. This allows developers to inject structured, type-safe mock entities with a single function call for rapid prototyping and unit testing, without writing a single rawINSERTstatement:// Scaffold a batch of dummy tasks and execution logs in one line
robot_kanban::generate_sample_data(&ctx, SampleDataPlan::small()).await?;
Scenario 2: JSON-Based Dynamic Filtering (filter_with_json)
What it does: Accepts a JSON object to dynamically construct WHERE clauses at runtime. An empty {} acts as a wildcard (no filter), enabling a single code path for both search and full-load.
// service.rs — Unified search/load query
let search_json = if let Some(ref term) = search_term {
let escaped_name = serde_json::Value::String(term.clone());
format!(r#"{{"name": {}}}"#, escaped_name) // → {"name": "calibrate"}
} else {
r#"{}"#.to_owned() // → {} (wildcard)
};
let select = Q::tasks()
.filter_with_json(&search_json);
| Input | JSON | Generated SQL |
|---|---|---|
| No search | {} | SELECT ... FROM task_data WHERE (version > 0) |
calibrate | {"name": "calibrate"} | SELECT ... FROM task_data WHERE (version > 0) AND (name LIKE '%calibrate%') |
Applied in: /search or /s command — filters the Kanban board in real-time.
Scenario 3: Faceted Aggregation (facet_by_status_as)
What it does: Attaches a sub-query that computes aggregate counts grouped by a relation (status), all within a single database round-trip alongside the main entity query.
// service.rs — Single query fetches tasks + status counts
let select = Q::tasks()
.comment(search_comment)
.filter_with_json(&search_json)
.facet_by_status_as("status_stats",
// This sub-query could easily be extracted into a semantic helper method
// e.g. `TaskStatusRequest::build_count_stats()` for reuse across the app
Q::task_status().comment("Count status").count_tasks()
);
let all_tasks = select.execute_for_list(&self.ctx).await?;
// Access facet results from the same SmartList
if let Some(facet_list) = all_tasks.facet("status_stats") {
for record in facet_list.iter() {
let status_id = record.get("id");
let count = record.get("count_tasks");
}
}
💡 Pro Tip (Semantic Encapsulation): Notice how
Q::task_status().count_tasks()is passed directly. Because TeaQL queries are strongly-typed data structures, you can effortlessly extract these aggregations into reusable, semantic helper methods.// 1. Encapsulate the query logic into a reusable semantic method
impl TaskStatusRequest {
pub fn build_count_stats() -> Self {
Q::task_status().comment("Count status").count_tasks()
}
}
// 2. Compose it cleanly in your main business logic
let select = Q::tasks()
.filter_with_json(&search_json)
.facet_by_status_as("status_stats", TaskStatusRequest::build_count_stats());This allows you to compose massive, multi-layered TeaQL queries dynamically without polluting your business logic. (Note: The
E::Expression API provides the exact same composability for field-level conditions and evaluations!)
Generated SQL (3 queries in one round-trip):
-- 1. Main entity query
SELECT id, name, version, status AS status_id, platform AS platform_id
FROM task_data WHERE (version > 0)
-- 2. Facet: load status reference data
SELECT id, name, code, color, display_order, progress, version FROM task_status_data WHERE (version > 0)
-- 3. Facet: aggregate task counts per status
SELECT status, COUNT(*) AS count_tasks
FROM task_data WHERE (version > 0) AND (version > 0) AND (status IN (1, 1001, 1002, 1003, 1004)) GROUP BY status
Applied in: Board reload — the Planned/Process/Done count badges and task lists are all populated from this single query.

Scenario 4: Entity Factory (Q::tasks().new_entity())
What it does: Creates a new entity instance pre-wired with the runtime context, ready for field population and persistence.
// task/logic.rs — Encapsulated factory method with DDD validation
impl Task {
pub fn create(cmd: &CreateTaskCommand, next_id: u64, ctx: &UserContext) -> Result<Self, AppError> {
let mut task = Q::tasks().new_entity(ctx);
task.update_id(next_id)
.update_name(cmd.name.clone())
.update_version(1_i64)
.update_status_to_planned() // Safe API: raw update_status_id(1) is blocked by the compiler
.update_platform_id(1_u64);
Ok(task)
}
}
Generated SQL:
INSERT INTO task_data (id, name, version, status, platform)
VALUES (1, 'calibrate sensor', 1, 1, 1)
Applied in: bare input <name> or /add command — creates a new task in Planned status.
Scenario 5: ID Space Generation (RusqliteIdSpaceGenerator)
What it does: Generates globally unique, monotonically increasing IDs per entity type using a dedicated SQLite sequence table — no auto-increment column needed.
// service.rs — add_task()
let next_id = self.ctx.next_id_for::<Task>()?;
Applied in: bare input <name> or /add command — each new task receives a unique ID from the Task ID space.
💡 Pro Tip: The Universal
UserContextNotice how we retrieve the ID generator fromself.ctx? TheUserContextobject is pervasive throughout your application's domain layer and request lifecycle. Because it is visible everywhere, it acts as the perfect dependency injection container.You can integrate any external resources directly into
UserContext, such as:
- Redis caching layers
- External API clients
- Email / SMS service clients
- Internationalization (i18n) resources
Simply use
ctx.insert_resource(...)at initialization, and use extension traits to expose type-safe, domain-specific methods anywhere in your business logic.
Scenario 6: Domain Behavior & Cascading Save (DDD Aggregate Root)
What it does: Allows attaching rich domain logic directly to generated entities using Rust's Native Extension Traits, and securely saving the entire Aggregate Root graph in a single atomic transaction.
// service.rs — Executing a DDD behavior
let mut task = Q::tasks().with_id_is(id).execute_for_one(&self.ctx).await?;
// 1. Invoke pure domain method (updates internal state)
let next_status = task.transition_status(&cmd)?;
// 2. Generate a child log entity via domain behavior
let log = task.generate_execution_log("STATUS_CHANGED", &detail, &self.ctx);
// 3. Attach the child to the Aggregate Root's collection
task.task_execution_log_list_mut().push(log);
task.set_comment("Move task status");
// 4. Graph persistence: Recursively saves the Task AND inserts the new child Log!
task.save(&self.ctx).await?;
Applied in: /mv command — enabling clean, expressive state mutations directly on Task objects.
Scenario 7: Partial Projections & Aggregations (return_type::<T>())
What it does: Tells TeaQL to deserialize query results into a custom data transfer object (DTO) instead of the default generated entity. This is vital when executing partial selects or complex groupings where the returned shape no longer matches the full entity.
// Define a custom DTO for aggregations or partial fields
#[derive(TeaqlEntity)]
pub struct StatusStats {
pub status: i32,
pub task_count: i64,
}
// Fetch custom projection instead of raw Task
let stats = Q::tasks()
.select_status()
.count_id_as("task_count")
.group_by_status()
.return_type::<StatusStats>()
.execute_for_list(&ctx).await?;
Applied in: High-performance dashboard rendering — avoids full-entity deserialization overhead when projecting lightweight summaries or grouped counts.
Scenario 8: Audited Soft-Delete (mark_as_delete)
What it does: Deletes an entity using the rich domain object rather than raw IDs. By chaining mark_as_delete() and set_comment() directly on the entity, TeaQL enforces optimistic concurrency (via the entity's current version) and gracefully propagates the deletion context to the EntityEventSink for audit logging.
// service.rs — delete_task()
let task_name = task.name().to_string();
task.mark_as_delete().set_comment(format!("Delete task '{}'", task_name));
task.save(&self.ctx).await?;
Generated SQL:
UPDATE task_data SET version = -2 WHERE id = 1 AND version = 1
TeaQL uses a soft-delete pattern — version is set to a negative value rather than removing the row, preserving audit history.
Applied in: /del command.
Scenario 9: Comment Chain Propagation (.comment())
What it does: Attaches human-readable intent annotations to queries. When queries have nested sub-queries (e.g., facets), comments propagate down the chain with -> separators, creating a full trace of query intent.
// service.rs — Comments propagate through facet sub-queries
let select = Q::tasks()
.comment("Get active tasks") // Parent comment
.filter_with_json(&search_json)
.facet_by_status_as("status_stats",
Q::task_status().comment("Count status") // Child comment
.count_tasks()
);
Resulting log trace chain:
[Get active tasks] → main task query
[Get active tasks->status_stats->Count status] → facet status lookup
[Get active tasks->status_stats->Count status] → facet aggregate count
The TUI renders these traces in real-time with syntax-highlighted colors — timestamp, user context ([philip]), comment chains, result summaries, and elapsed times are each distinctly colored:
[12:06:00.225]-[philip]-[0.184ms]-[DEBUG]-SqlLogEntry - [Get active tasks] - [5*Task] SELECT ...
[12:06:00.226]-[philip]-[0.138ms]-[DEBUG]-SqlLogEntry - [Get active tasks->status_stats->Count status] - [3*TaskStatus] SELECT ...
Applied in: Every query in the application — enables real-time SQL auditing from the TUI log panel.
