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:
Instead of hiding database behavior behind an opaque ORM, this demo shows the full execution path of a domain action:
We are thrilled to announce the release of TeaQL 1.0.0! This milestone marks the stabilization of our core APIs, major performance improvements, and a completely redefined model-driven development experience across both Rust and Java (Spring Boot) ecosystems.
TeaQL 1.0.0 focuses on API Elegance, Developer Ergonomics, AI-Native Workflows, and Transaction Safety.
TeaQL Rust uses runtime providers to keep generated business APIs separate from storage execution.
The application code should use the generated API. The runtime should decide how that API executes against PostgreSQL, MySQL, SQLite, embedded SQLite, or memory.
| Provider | Best for |
|---|---|
| SQLx PostgreSQL | production-grade backend services, complex queries, transactions, aggregation |
| SQLx MySQL | enterprise MySQL systems and migration scenarios |
| SQLx SQLite | local-first apps, tests, lightweight services |
| rusqlite SQLite | embedded, router, edge, sync execution, multi-architecture devices |
| MemoryRepository | no-database tests, demos, fast model validation |
PostgreSQL is the strongest default for production backend systems that need:
TeaQL's SQLx PostgreSQL provider keeps PostgreSQL-specific execution behind the repository boundary.
MySQL remains common in enterprise business systems. TeaQL's SQLx MySQL provider is intended for teams that want generated business APIs while staying on a familiar MySQL backend.
This is especially useful when moving away from handwritten mapper-heavy persistence without moving the database first.
SQLite has two important TeaQL paths.
SQLx SQLite is useful for async local-first apps, integration tests, small services, and portable demos.
rusqlite is useful when synchronous embedded SQLite is a better fit:
Not every generated API test needs a database.
MemoryRepository gives TeaQL a no-database path for:
The goal is to test generated API behavior without requiring a database server.
The provider is registered below UserContext:
Generated service crate
-> RuntimeModule
-> UserContext
-> Repository API
-> selected provider
Generated crates can expose helpers for module registration, behavior/checker registration, provider-backed runtime setup, and schema bootstrap.
Without a provider boundary, application code tends to mix business intent with database details.
With providers, the generated API remains stable:
Q::platforms()
.select_merchant_list_with(Q::merchants().select_name())
.execute_for_list(&ctx)
.await?;
The runtime decides whether that request executes through PostgreSQL, MySQL, SQLite, rusqlite, or memory.
That is the point of TeaQL's multi-database runtime direction.
TeaQL started with a mature Java implementation. The Rust work does not try to clone every Java framework feature. It carries over the high-level programming model and rebuilds the runtime in Rust.
That shift matters.
Java proved the generated business API style. Rust turns that style into a runtime direction for PostgreSQL, MySQL, SQLite, embedded SQLite, and memory-backed tests.
The useful Java ideas are not Spring-specific. They are TeaQL-specific:
Q APIs;SmartList style result metadata;Rust implements those ideas with Rust types, traits, crates, and provider registration.
Some Java features should not be copied blindly:
Rust needs a Rust-native shape. The runtime should be explicit, crate-based, and provider-backed.
The Rust runtime is split by responsibility:
teaql-core for metadata, values, query model, entity traits, and SmartList<T>;teaql-sql for SQL compilation and dialect behavior;teaql-runtime for UserContext, repositories, checkers, events, relation enhancement, graph writes, and memory execution;teaql-macros for #[derive(TeaqlEntity)];This keeps the core runtime separate from database adapters.
teaql-code-gen can emit Rust service crates. A generated crate can include:
Q facade;Application code then uses generated APIs:
let platforms = Q::platforms()
.select_merchant_list_with(
Q::merchants()
.select_name()
.which_names_contain("TeaQL"),
)
.execute_for_list(&ctx)
.await?;
The Rust direction is provider-based:
The generated API should stay stable while the provider changes below it.
The Rust runtime is already useful for core generated-domain paths:
Q API validation against SQLite and PostgreSQL paths.Full Java feature parity is not the only metric. The better metric is whether Rust has a coherent generated business API runtime.
That is now the direction.
TeaQL is a generated business API platform for systems where the domain model is more important than the storage plumbing around it.
Instead of asking developers to repeatedly write repositories, SQL fragments, DTO stitching, relation loading code, and validation glue, TeaQL starts from a domain model and generates APIs that read like business intent.
The current positioning is simple:
Generated Business APIs. Java-proven. Rust-powered. Multi-database ready.
TeaQL separates the business API from the runtime provider.
Domain Model
-> TeaQL Generator
-> Generated Q API
-> Runtime Provider
-> MySQL / PostgreSQL / SQLite / Memory / Edge / Agent
Application code should talk to generated APIs. Runtime code should decide how those APIs execute against a database, memory repository, embedded store, or service runtime.
Depending on the target stack, TeaQL can generate:
In Java, this appears as fluent generated request APIs. In Rust, generated crates expose Q::entities() style query builders over teaql-rs.
Large business systems repeat the same concepts across many screens and services:
Without a generated business API layer, those concepts spread across SQL, mapper XML, repositories, service methods, DTOs, validators, and frontend-specific response shaping.
TeaQL keeps the model vocabulary visible in the code path.
Java TeaQL is the mature, proven path for enterprise systems and Spring Boot services.
Rust TeaQL is the runtime direction for generated domain APIs across PostgreSQL, MySQL, SQLite, embedded SQLite, and memory-backed tests.
The two stacks are not identical, and they do not need to be. They share the programming model: generated APIs over a domain model, executed through a runtime boundary.
User userOrderInfo = Q.users()
.filterWithId(userId)
.countOrder()
.statsFromOrder("statusWithCount", Q.orders().count().groupByOrderStatus())
.selectOrderList(
Q.ordersWithId()
.selectOrderId()
.selectDate()
.offset(0, 10)
.countLineItems()
)
.execute(context);
This is not just a query. It is the shape of a business page expressed through generated APIs.
TeaQL turns domain models into stable business APIs that humans and AI tools can read, compose, and run across Java, Rust, and multiple database providers.
TeaQL Rust supports both SQLx and rusqlite because they solve different deployment problems.
This is not duplication. It is provider choice.
The generated business API should remain stable while the runtime provider changes underneath.
SQLx is a strong fit for async server-side Rust services.
TeaQL uses SQLx providers for:
SQLx fits services that already run inside an async runtime, use connection pools, and need production backend behavior such as transactions, schema bootstrap, row decoding, and database-specific SQL execution.
Good SQLx targets:
rusqlite is a strong fit for synchronous embedded SQLite.
That matters for deployments where the database is not a remote backend service:
In those cases, an async pool-oriented provider may be unnecessary weight. A synchronous SQLite provider can be easier to deploy and reason about.
The application should still use generated APIs:
let rows = Q::orders()
.select_self()
.select_line_item_list_with(Q::line_items().select_self())
.execute_for_list(&ctx)
.await?;
The question of SQLx SQLite versus rusqlite SQLite should be a runtime decision, not a reason to rewrite business query code.
The boundary looks like this:
Generated Q API
-> UserContext
-> Repository API
-> SQLx PostgreSQL / SQLx MySQL / SQLx SQLite / rusqlite SQLite
This keeps database execution below generated business intent.
Use SQLx when:
Use rusqlite when:
TeaQL is not only about database support. It is about keeping domain APIs independent of storage execution.
Supporting both SQLx and rusqlite lets the same generated model fit more deployment shapes without changing the programming model.
TeaQL is a data layer for applications where the domain model is the center of the system rather than a thin mapping over tables. The current Rust implementation carries over ideas from the Java TeaQL stack, but with a smaller scope: PostgreSQL, SQLite, a Rust-native query AST, generated typed APIs, and no web framework dependency.
The goal is direct: instead of writing most application data access as handmade repository methods or raw SQL fragments, a domain model can generate a Rust crate with entity types, relation metadata, query builders, checker hooks, behavior hooks, and graph-save entrypoints.
Application code then works with a high-level API:
let platforms = Q::platforms()
.select_merchant_list_with(
Q::merchants()
.select_name()
.which_names_contain("TeaQL"),
)
.execute_for_list(&ctx)
.await?;
This is not meant to replace every way of using SQL from Rust. If a service is
mostly carefully tuned SQL, direct sqlx is probably a better fit. If the
preferred abstraction is an ORM with a large Rust ecosystem, Diesel or SeaORM
will be more familiar. TeaQL is aimed at a different case: large domain models
where repeated relation loading, graph persistence, validation, and statistics
queries become their own layer of application logic.
The original pressure came from systems where the same entity model needed to support several kinds of behavior:
merchant.platform or
platform.merchant_list;You can build all of that by hand, but the code tends to become repetitive in two places. First, relation names and field names are repeated across query methods, repository code, and validation code. Second, application developers end up switching between typed domain objects and untyped row maps. TeaQL tries to keep the generated surface typed while letting the runtime keep a generic query and graph model internally.
For example, a generated service crate exposes Q::platforms() and
Q::merchants() rather than asking application code to construct
SelectQuery::new("Platform") directly. Low-level query objects still exist,
but they are not the normal application-level API.
The Rust workspace is split into small crates:
teaql-core: values, records, entity descriptors, query AST, expressions,
commands, and SmartList<T>;teaql-sql: SQL compilation and dialect-neutral compiled query types;teaql-runtime: UserContext, repository resolution, behavior hooks,
checker hooks, graph writes, relation enhancement, events, and optional SQLx
executors;teaql-macros: #[derive(TeaqlEntity)] for descriptors and typed
record/entity mapping;teaql-provider-sqlx-postgres, teaql-provider-sqlx-sqlite, teaql-provider-sqlx-mysql, and teaql-provider-rusqlite: provider adapters and dialect-specific execution paths.Generated crates sit above those runtime crates. A generated CRM or ERP service,
for example, exports entities such as Platform and Merchant, a Q query
facade, behavior skeletons, checker skeletons, repository registration, and
runtime module assembly helpers.
TeaQL has a generic query AST, but generated code provides a domain-specific facade. Instead of this in application code:
let query = SelectQuery::new("Merchant")
.project("id")
.project("name")
.filter(Expr::contains("name", "tea"));
the generated API can expose:
let merchants = Q::merchants()
.select_name()
.which_names_contain("tea")
.order_by_create_time_desc()
.page(1, 20)
.execute_for_list(&ctx)
.await?;
Relation loading uses the same style:
let platforms = Q::platforms()
.select_merchant_list_with(
Q::merchants()
.select_name()
.select_platform(),
)
.execute_for_list(&ctx)
.await?;
One implementation detail mattered here: when a child is attached to a parent
relation list, the reverse object relation should be populated too. In the
example above, each merchant in platform.merchant_list should have its
platform relation set. Otherwise, the result is typed but not really a domain
object graph.
TeaQL has a save_graph path for committing complex objects. In generated
crates, application code can call a typed save helper:
merchant
.update_name("TeaQL Merchant")
.update_platform_id(1_u64)
.save(&ctx)
.await?;
Internally, the runtime turns that object into a graph plan. The plan classifies nodes by entity and operation: create, update, delete/remove, or reference. It then batches compatible work where possible and runs the graph write inside a transactional executor.
The interesting part is not only inserting children. Updating a graph means answering questions like:
The Rust runtime currently supports nested create/update graph writes, reference-only nodes, explicit remove nodes, keep-missing relation metadata, duplicate child-id rejection, and transaction rollback for SQLite and PostgreSQL SQLx executors.
For local development and generated-service tests, TeaQL can bootstrap a schema from entity descriptors:
ctx.ensure_sqlite_schema().await?;
The current scope is intentionally conservative. It creates missing tables and adds missing columns. It does not try to be a destructive migration tool: no column drops, no primary-key rebuilds, and no automatic type rewrites.
That line is important because generated domain models change frequently. The bootstrap path should be safe enough for local and CI use, not pretend to replace a real production migration process.
A checker is not just a validator that rejects a row. It can inspect an object, add structured check results, and sometimes fix fields before persistence.
The generated Rust checker support lets application code write typed checker
logic instead of manually reading from a Record:
impl MerchantCheckerLogic for MerchantNameChecker {
fn check_and_fix_merchant(
&self,
_ctx: &UserContext,
entity: &mut Merchant,
status: CheckObjectStatus,
location: &ObjectLocation,
results: &mut CheckResults,
) {
if status.is_create() {
self.required_text(&entity.name(), "name", location, results);
}
self.min_string_length(&entity.name(), "name", 3, location, results);
if entity.name() == "fix" {
entity.update_name("fixed");
}
}
}
The runtime still stores the common checker interface at the record level, but the generated adapter maps records into typed entities before calling the checker. That keeps the public application code close to the domain model while preserving a generic runtime path.
TeaQL queries can carry aggregate projections and relation aggregate metadata. The current runtime supports simple aggregates, grouped aggregates, Decimal results for SQL aggregate output, relation count/statistic attachment, and database-column-to-entity-property mapping for relation aggregate keys.
The generated Q APIs can express both simple statistics and relation
statistics. For example, a service can count child rows from a parent query
without asking application code to hand-build the join every time.
This area is useful but still evolving. The runtime has working SQL and memory paths for the core cases, while broader Java parity still needs more work around memory subqueries and richer relation aggregate shapes.
The most obvious tradeoff is generated code. TeaQL generates a lot of Rust. That cost shows up as compile time, larger diffs, and the need to keep templates disciplined. The benefit is that application code gets a stable, typed facade over a large domain model.
Another tradeoff is that the runtime is not purely compile-time checked. The generated APIs are typed, but the runtime still has a generic query AST, record model, and descriptor registry. That gives it flexibility for dynamic projections, aggregate rows, JSON-style search, and graph planning, but it means some mistakes are caught by generated crate tests rather than by Rust types alone.
The final tradeoff is scope. The Rust rewrite is not trying to clone every Java TeaQL feature or support every database. PostgreSQL and SQLite are enough for now. Web rendering, GraphQL integration, and broad database dialect support are outside the current Rust scope.
The current Rust runtime and generated crate tests cover:
SmartList<T>;Q APIs against SQLite;The public examples can be run with:
cargo run -p teaql-examples --bin sqlite_schema_crud
cargo run -p teaql-examples --bin sqlite_relations_graph
The first command shows schema bootstrap and CRUD against in-memory SQLite. The second saves an object graph and reloads nested relations.
The biggest gaps are:
Those gaps are real. They are better kept visible than hidden behind a larger feature list.
Most Rust database libraries are good at one of two layers: explicit SQL, or a database-centric ORM. TeaQL explores a third shape: generated domain APIs over a generic runtime that understands entity graphs, relation enhancement, validation, and statistics.
That shape will not fit every codebase. It is most useful when the model is large enough that the generated API becomes an asset, and when the team wants the same domain semantics to appear in queries, graph writes, checkers, and schema bootstrap.
TeaQL generates query APIs such as Q::platforms() and Q::merchants(). That
immediately raises a fair question: why not just write SQL?
The short answer is that TeaQL is not trying to replace handwritten SQL everywhere. Generated query APIs are useful when the repeated work is not the SQL text itself, but the domain semantics around the SQL: field names, relation names, relation loading, statistics, validation, JSON search, and graph-shaped results.
There are many cases where handwritten SQL is the right tool:
TeaQL should not get in the way of those cases. The Rust runtime keeps a generic query AST and SQL compiler, but the design does not require every query to be expressed through generated methods.
If explicit SQL is the clearest abstraction, use explicit SQL.
Generated APIs become useful when a large domain model produces many ordinary queries with the same repeated structure:
let merchants = Q::merchants()
.select_name()
.select_platform_with(Q::platforms().select_name())
.which_names_contain("tea")
.order_by_create_time_desc()
.page(1, 20)
.execute_for_list(&ctx)
.await?;
This is not shorter than SQL in every case. The value is that the query is tied to the generated domain model:
SmartList<T> collections.That matters when the model is large and the same entity relationships appear across many services, pages, and business workflows.
A generated API can encode relation semantics that plain SQL does not name.
For example:
let platforms = Q::platforms()
.select_merchant_list_with(
Q::merchants()
.select_name()
.select_platform(),
)
.execute_for_list(&ctx)
.await?;
The application is not just asking for a join. It is asking for a platform list
where each platform contains a merchant_list, and each merchant can carry its
reverse platform relation when selected. That object shape is a domain result,
not only a SQL result set.
The runtime still compiles to SQL and fetches records. The generated API gives the caller a domain-oriented way to request the shape.
TeaQL still has SelectQuery internally. Application code can use it directly,
but generated crates should prefer the entity-specific Q entrypoints:
let platforms = Q::platforms()
.select_merchant_list_with(
Q::merchants()
.which_names_are("TeaQL Merchant"),
)
.execute_for_list(&ctx)
.await?;
This keeps the application out of string-based construction such as
SelectQuery::new("Merchant") for ordinary code. The generic query type remains
available for runtime and escape-hatch scenarios.
Large business systems rarely stop at list queries. They also need counts, sums, grouped aggregates, relation counts, and derived summary rows.
A generated API can give those operations domain names:
let platforms = Q::platforms()
.select_name()
.count_merchant_list_as("merchant_count")
.execute_for_list(&ctx)
.await?;
The runtime can attach relation aggregate results back to parent rows, while the generated request builder keeps the caller focused on domain concepts.
This does not remove the need for handwritten reporting SQL. It gives ordinary domain statistics a consistent path.
If generated APIs are useful, why not make the whole runtime typed?
Because TeaQL still needs dynamic behavior:
The generated layer is typed for application ergonomics. The runtime layer is generic so it can plan, compile, enhance, validate, and execute across many generated models.
Generated code is not free:
TeaQL tries to offset that with explicit generated code, local runtime crates, and generated service tests that run real SQLite scenarios. The code is not hidden behind a server or reflection system; it is Rust source that can be inspected.
Still, generated APIs should be used where they buy something. A small service
with ten queries may be better served by direct sqlx.
The boundary I use is this:
Q API;TeaQL's design only works if the lower level remains available.
The goal is not to win an argument against SQL. SQL remains the execution language and often the best authoring language.
The goal is to avoid making every application workflow manually rediscover the
same domain relationships. In a large model, the generated API becomes a shared
vocabulary: Q::platforms(), select_merchant_list_with(...),
have_platform(), count_merchant_list_as(...), and so on.
That vocabulary is where TeaQL is useful. Not because SQL is bad, but because large domain models need more than SQL strings to stay coherent.
Object graph persistence looks simple until the first update path arrives. A new parent with new children is close to a recursive insert. A real business object graph is not.
TeaQL's save_graph runtime exists because the runtime needs to understand
entity identity, relation ownership, reference semantics, missing children,
soft delete, and transaction boundaries at the same time.
The simple case is a parent object with children:
let order = Order {
id: 1,
version: 1,
name: "graph-order".to_owned(),
lines: SmartList::from(vec![
OrderLine {
id: 10,
order_id: 0,
name: "first-line".to_owned(),
product_id: 100,
product: Some(Product {
id: 100,
name: "keyboard".to_owned(),
}),
},
]),
};
repo.save_entity_graph(order)?;
The runtime can create the product, create the order, attach the line to the order, and create the line. If that were the whole problem, graph save would be a convenience wrapper over insert.
The harder cases are the reason TeaQL treats graph persistence as a plan.
When a graph reaches the runtime, each node has to be classified:
Create: the row does not exist yet and should be inserted;Update: the row exists and selected fields should be changed;Reference: the row must exist, but the graph should not mutate it;Remove: the row is explicitly removed from a relation or graph.Those states are not interchangeable. A referenced row should fail if it does not exist. A removed row should not silently become an update. A create with a duplicate id should not be treated as a harmless no-op.
TeaQL exposes this through graph operation hints and validates the state against the repository when needed.
An object relation may attach a foreign key, or it may be detached. A one-to-many relation may delete missing children, or it may keep missing children untouched.
That relation metadata matters during updates:
#[teaql(relation(
target = "OrderLine",
local_key = "id",
foreign_key = "order_id",
many,
delete_missing = false
))]
lines: SmartList<OrderLine>,
With delete_missing = false, sending an order with one line does not mean all
other order lines should be deleted. Without that metadata, a graph update can
look correct in tests and still be dangerous in production.
Attach metadata is similar. Some relations express ownership and should write a foreign key. Others are navigational and should not.
The most common graph update question is what to do with children that were in the database but are missing from the submitted graph.
There are several possible meanings:
TeaQL avoids guessing from the shape alone. Relation descriptors carry the policy. The graph planner uses that policy to decide whether to keep missing rows or plan a remove/soft-delete operation.
The runtime builds a mutation plan before executing:
Order:
update: [id=1, fields=name]
OrderLine:
create: [id=12]
update: [id=10, fields=name]
delete: [id=11]
Product:
reference: [id=100]
This plan is useful for three reasons.
First, it lets TeaQL validate relation semantics before making changes.
Second, it allows compatible mutations to be batched by entity type and updated field set.
Third, it gives developers a debugging surface. UserContext::plan_for_save_graph()
can be used to inspect what the runtime thinks it is going to do.
A graph write that touches multiple tables has to be transactional. If a child insert fails after the parent update succeeds, the application now has a partial business object.
TeaQL requires graph writes to go through a transactional executor. SQLite and PostgreSQL SQLx paths both have transaction-boundary support. The test suite verifies rollback behavior for graph writes.
That requirement is deliberately strict. A graph write without a transaction is usually worse than no graph-write helper at all.
Generated service crates should not force application code to build raw graph nodes by hand. The current generated API can work with typed entities and turn them into graph nodes internally:
merchant
.update_name("TeaQL Merchant")
.update_platform_id(1_u64)
.save(&ctx)
.await?;
The runtime still uses a generic graph representation after extraction. That is intentional. The typed layer is for application ergonomics; the generic layer is for planning, batching, validation, and execution.
This is not a general object database. The database remains relational. TeaQL does not try to hide SQL or table design completely.
It is also not a replacement for explicit migrations. Graph writes operate
against descriptors and repositories. Schema evolution is handled separately by
the additive ensure_schema path for local development and tests, or by a
proper migration process in production.
In small systems, graph save can be overkill. In large business systems, object graphs are often the natural unit of change: an order and its lines, a platform and its merchants, a service contract and its documents.
The hard part is not creating the first version. The hard part is updating the second version without losing data, silently mutating references, or leaving a partial graph after a failure.
That is why TeaQL treats object graph persistence as planning first and execution second.
TeaQL Rust reaches v0.7.0. The crates are published and documented.
teaql-core // Entity traits, metadata, and core queries
teaql-sql // SQL dialect and AST-to-SQL compiler
teaql-runtime // UserContext, registry, graph writes, checkers, and events
teaql-macros // Derive macro for entity descriptors
teaql-provider-sqlx-postgres // PostgreSQL SQLx provider adapter
teaql-provider-sqlx-sqlite // SQLite SQLx provider adapter
teaql-provider-sqlx-mysql // MySQL SQLx provider adapter
teaql-provider-rusqlite // synchronous SQLite rusqlite provider adapter
Each crate has a standalone readme with examples.
let cache = AggregationCache::new(CacheBackend::Memory)
.namespace("daily_revenue")
.ttl(Duration::from_secs(300));
let revenue: Decimal = cache
.get_or_compute(|| compute_daily_revenue())
.await?;
Namespaced caches prevent key collisions across different aggregations.
let ctx = UserContext::new()
.with_sql_debug(true);
// Logs: SELECT id, name FROM user_t WHERE age >= ?
// Params: [18]
Debug output shows the final SQL and bound parameters. Toggle per request via UserContext.
let expr = User::age()
.gte(18)
.and(User::status().in_vec(&["active", "premium"]))
.or(User::role().eq("admin"));
in_vec, between, like, and nested and/or groups are now supported.
Quick patch for documentation links. No functional changes.
Repository module split and open source cleanup.
TeaQL Code Gen learned Rust. One model definition now produces both Java and Rust artifacts.
templates/
java/
entity.java.vm
repository.java.vm
rust/
entity.rs.vm
query.rs.vm
Each stack has isolated templates. No cross-language leakage.
// Generated from the same .teaql model
pub struct User {
pub id: u64,
pub name: String,
}
impl TeaQLEntity for User {
const TABLE: &str = "user_t";
}
let names: Vec<String> = db.query(User::name())
.filter(User::active().eq(true))
.list()
.await?;
The generator produces typed projections. User::name() returns String, User::class() returns User.
let stats = db.query(Order::class())
.group_by(Order::status())
.agg(Order::total().sum())
.agg(Order::id().count())
.list()
.await?;
Group-by and rollup DSLs are fully typed. The macro validates aggregation compatibility at compile time.
let user = db.load(User::class(), id)
.with(User::orders())
.await?;
User::orders() is generated from the model's relation definition. No hand-written joins.
The Rust runtime now supports typed graph mutations and a fluent query DSL.
let mut graph = EntityGraph::new();
graph.add(User::new().name("Alice"));
graph.add(Order::new().user_id("Alice.id"));
db.save_graph(&graph).await?;
The runtime validates foreign keys, plans insertion order, and executes within a transaction.
let id_gen = SqlIdSpace::new("user_id_seq", 1000);
let batch = id_gen.next_batch(10).await?;
Reserves id ranges in bulk. Reduces database round-trips during bulk inserts.
let total: Decimal = db.query(Order::total())
.filter(Order::status().eq("paid"))
.sum()
.await?;
rust_decimal replaces f64 for all aggregate results. Eliminates floating-point drift in financial calculations.
let users = db.query(User::class())
.filter(User::age().gte(18))
.order_by(User::created_at().desc())
.limit(20)
.list()
.await?;
Methods chain naturally. The macro expands field references into type-safe column identifiers at compile time.
Graph mutations emit checker events:
This hooks into the transactional planner for complex business rules.
TeaQL expands into Rust. The core runtime ships with procedural macros, dual database support, and a unified id space.
Java served us well for enterprise backends. Rust brings zero-cost abstractions, memory safety without GC, and native performance. The goal is not to replace the Java stack but to offer a lean alternative for performance-critical services.
#[derive(TeaQLEntity)]
struct User {
id: u64,
name: String,
}
The TeaQLEntity macro derives:
| Database | Schema Ensuring | Status |
|---|---|---|
| SQLite | ensure_schema | Ready |
| PostgreSQL | ensure_schema | Ready |
let db = SqliteBackend::open("app.db").await?;
db.ensure_schema::<User>().await?;
All entity ids moved from i32 to u64:
Graph writes and a query DSL are already in progress.