Skip to main content

8 posts tagged with "runtime"

View All Tags

Enforcing Request Policy at the Runtime Boundary

· 6 min read

In a multi-tenant business system, the dangerous query is often not obviously dangerous.

A developer writes a useful request:

Q.candidates()
.selectName()
.selectEmail()
.filterBySkill("Java")
.page(1, 20)
.executeForList(ctx);

The request looks typed, generated, and harmless. But in a platform like Multi Talent, a candidate belongs to a customer account, a workspace, a recruiter team, and often a legal region. A useful query becomes unsafe if it can see outside those boundaries.

That is why TeaQL treats request execution as a runtime boundary, not just a method call.

The Multi Talent Problem

Imagine Multi Talent as a SaaS platform for recruiting agencies and enterprise hiring teams.

The same TeaQL model may include:

  • candidates;
  • talent profiles;
  • resumes and attachments;
  • interview records;
  • offer workflows;
  • customer accounts;
  • recruiter teams;
  • regional compliance metadata.

The query language should stay expressive. A team should be able to search candidates by skill, location, availability, interview status, and related job opening.

But every query must also respect infrastructure and customer boundaries:

  • customer A must never read customer B's candidates;
  • a recruiter can only see candidates assigned to their team or allowed pool;
  • regional data residency policy may limit which records can be loaded;
  • raw SQL escape hatches should not bypass tenant rules;
  • unlimited list requests should not become infrastructure abuse.

Those rules should not be copied into every controller.

The Wrong Place for the Rule

One option is to add filters wherever a query is written:

Q.candidates()
.filterByCustomer(ctx.currentCustomer())
.filterByRecruiterTeam(ctx.currentTeam())
.filterBySkill("Java")
.executeForList(ctx);

This works until one path forgets the filter. It also asks every feature author to understand every infrastructure and data-protection rule.

Another option is to hide the query behind service methods. That can work for some workflows, but TeaQL deliberately gives teams a generated request language. The runtime should make that language safe instead of forcing teams to abandon it.

The Runtime Boundary

TeaQL Java runtime exposes a dedicated UserContext extension point for this final step:

protected <T extends Entity> SearchRequest<T> enforceRequestPolicy(SearchRequest<T> request) {
return request;
}

Every normal request execution path goes through this hook before the request is submitted to the repository:

SearchRequest
-> UserContext
-> enforceRequestPolicy(...)
-> Repository
-> database provider

That placement matters. It is late enough to see the actual request that is about to execute, but still early enough to change it or reject it.

TeaQL Rust has the same runtime-boundary idea through RequestPolicy on UserContext. The Rust hook is platform-scoped and runs after entity-scoped repository behavior, so it can make the final decision before the request reaches the provider.

A Multi Talent Policy

A Multi Talent project can extend its generated context and enforce the policy once:

public class MultiTalentUserContext extends MultiTalentGeneratedUserContext {

@Override
protected <T extends Entity> SearchRequest<T> enforceRequestPolicy(SearchRequest<T> request) {
request = super.enforceRequestPolicy(request);

String type = request.getTypeName();

if ("Candidate".equals(type) || "TalentProfile".equals(type)) {
request.appendSearchCriteria(
request.createBasicSearchCriteria(
"customerAccount",
Operator.EQUAL,
currentCustomerAccount()));

request.appendSearchCriteria(
request.createBasicSearchCriteria(
"recruiterTeam",
Operator.IN,
visibleRecruiterTeams()));
}

if ("ResumeAttachment".equals(type)) {
enforceRegionalAccess(type);
}

rejectDangerousRequestShape(request);
auditSensitiveRead(request);

return request;
}
}

The example is intentionally centralized. Feature code can still express the business query:

Q.candidates()
.selectName()
.selectEmail()
.filterBySkill("Java")
.page(1, 20)
.executeForList(ctx);

The runtime adds the customer and team boundaries before the repository sees the request.

Protecting More Than Rows

The hook is not only about tenant IDs.

In a real platform, request policy can protect infrastructure as well as data:

  • reject rawSql for normal users;
  • cap page size for list views;
  • block unlimited() on high-volume entities;
  • add region or data residency predicates;
  • require extra audit for sensitive reads;
  • remove unsafe relation loading for restricted roles;
  • attach request comments or markers for tracing.

For example:

private void rejectDangerousRequestShape(SearchRequest<?> request) {
if (request.getRawSql() != null && !currentUserCanUseRawSql()) {
throw new AccessDeniedException("Raw SQL is not allowed for this user");
}

Slice slice = request.getSlice();
if (slice != null && slice.getSize() > 200) {
slice.setSize(200);
}
}

This is infrastructure protection. It prevents one feature path from turning into a full-table scan, an accidental data export, or an expensive cross-tenant aggregation.

Why UserContext Is the Right Place

UserContext already knows the runtime facts needed to enforce policy:

  • the current customer or tenant;
  • the current operator;
  • roles, teams, and permissions;
  • request headers and trace ID;
  • client IP and proxy chain;
  • environment-level beans and services;
  • audit and logging services.

Repositories should not need to know every business permission model. Controllers should not need to repeat low-level safety rules. The generated request API should remain readable.

UserContext is the convergence point.

Auditing Sensitive Reads

Because the hook sees the final request, it is also a useful place to audit sensitive reads:

private void auditSensitiveRead(SearchRequest<?> request) {
if (!"Candidate".equals(request.getTypeName())) {
return;
}

auditTrail().recordRead(
traceId(),
currentCustomerAccountId(),
currentUserId(),
request.getTypeName(),
request.comment(),
getClientIp());
}

This does not replace business-level audit records such as "approved offer" or "revoked recruiter access." It complements them by making sensitive data access observable at the runtime boundary.

The Design Principle

Generated APIs should make business intent visible.

Runtime policy should make that intent safe to execute.

In Multi Talent, a query for candidates should read like a query for candidates. It should not be filled with repeated customer, team, residency, audit, and infrastructure rules. Those rules belong at the boundary where a request is submitted to the runtime.

That is what enforceRequestPolicy is for.

In Rust, the same idea is expressed as a RequestPolicy:

use teaql_core::{Expr, SelectQuery};
use teaql_runtime::{RequestPolicy, RuntimeError, UserContext};

pub struct MultiTalentPolicy;

impl RequestPolicy for MultiTalentPolicy {
fn enforce_select(
&self,
ctx: &UserContext,
query: &mut SelectQuery,
) -> Result<(), RuntimeError> {
if matches!(query.entity.as_str(), "Candidate" | "TalentProfile") {
let customer_id = ctx
.get_named_resource::<u64>("customer_account_id")
.copied()
.ok_or_else(|| RuntimeError::Policy("missing customer account".to_owned()))?;

let tenant_filter = Expr::eq("customer_account_id", customer_id);
query.filter = Some(match query.filter.take() {
Some(existing) => existing.and_expr(tenant_filter),
None => tenant_filter,
});
}

if query.raw_sql.is_some() {
return Err(RuntimeError::Policy(
"raw SQL is not allowed for normal users".to_owned(),
));
}

Ok(())
}
}

Register it during runtime assembly:

let ctx = teaql_runtime::UserContext::new()
.with_module(multi_talent::module_with_behaviors_and_checkers())
.with_request_policy(MultiTalentPolicy);

Java uses UserContext.enforceRequestPolicy. Rust uses RequestPolicy. The design principle is the same: TeaQL projects get a final, explicit place to protect the platform and the customer's data before a request reaches the repository.

TeaQL is not just an ORM

· 2 min read
TeaQL Code Gen
Core Contributor

It is tempting to describe TeaQL as an ORM because TeaQL knows about entities, relations, repositories, and databases.

That description is incomplete.

TeaQL is a generated business API layer. Persistence is one part of the system, but the main value is the generated domain language that sits above persistence.

What an ORM Usually Optimizes

Most ORM discussions focus on mapping:

  • classes to tables;
  • fields to columns;
  • relations to joins;
  • objects to rows;
  • transactions to persistence sessions.

Those are real problems. TeaQL also needs to solve them. But large business systems have another repeated problem: the same business request is rebuilt again and again in controllers, repositories, DTOs, SQL, validators, and frontend response logic.

What TeaQL Optimizes

TeaQL optimizes the business API surface.

It generates request APIs that can express:

  • selection;
  • nested relation loading;
  • filters;
  • list-existence queries;
  • pagination;
  • grouped statistics;
  • relation aggregates;
  • graph writes;
  • validation hooks;
  • runtime customization.

The generated API is meant to be read by backend engineers, domain engineers, reviewers, and AI coding tools.

Generated Business APIs

An ORM might make it easy to load an Order.

TeaQL aims to make it clear how a complete order page is assembled:

Q.orders()
.filterByMerchant(ctx.getMerchant())
.selectCustomer(Q.customers().selectName())
.selectLineItemList(Q.lineItems().selectSku().selectQuantity())
.countLineItems()
.orderByCreateTimeDescending()
.page(1, 20)
.executeForList(ctx);

The key is not whether this compiles to SQL. It does. The key is that the API names the business shape before the runtime turns it into storage operations.

Runtime Boundary

TeaQL also treats runtime behavior as part of the model:

  • user context;
  • tenant and permission policy;
  • cache behavior;
  • distributed locks;
  • logging and metrics;
  • validation;
  • event dispatch;
  • database provider selection.

That is why TeaQL has a runtime layer instead of only a mapper layer.

Why This Helps AI

AI tools should not need to guess SQL, join rules, table names, tenant filters, and response shapes from scattered code.

Generated APIs give AI tools a deterministic vocabulary:

  • call this field selector;
  • use this relation loader;
  • compose this query fragment;
  • execute through this runtime context;
  • do not bypass the provider.

That is a different goal from a traditional ORM.

The Short Version

TeaQL uses persistence mapping, but it is not defined by persistence mapping.

It is a generated business API platform that keeps domain intent visible while allowing the runtime provider to change underneath.

TeaQL Runtime Providers: PostgreSQL, MySQL, SQLite

· 2 min read
TeaQL Code Gen
Core Contributor

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 Matrix

ProviderBest for
SQLx PostgreSQLproduction-grade backend services, complex queries, transactions, aggregation
SQLx MySQLenterprise MySQL systems and migration scenarios
SQLx SQLitelocal-first apps, tests, lightweight services
rusqlite SQLiteembedded, router, edge, sync execution, multi-architecture devices
MemoryRepositoryno-database tests, demos, fast model validation

PostgreSQL

PostgreSQL is the strongest default for production backend systems that need:

  • transactions;
  • rich query behavior;
  • grouped aggregation;
  • Decimal/NUMERIC support;
  • schema bootstrap;
  • id-space generation;
  • production database tooling.

TeaQL's SQLx PostgreSQL provider keeps PostgreSQL-specific execution behind the repository boundary.

MySQL

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

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:

  • devices;
  • routers;
  • edge deployments;
  • appliance controllers;
  • local agent memory.

MemoryRepository

Not every generated API test needs a database.

MemoryRepository gives TeaQL a no-database path for:

  • unit tests;
  • model validation;
  • lightweight demos;
  • fast runtime simulation.

The goal is to test generated API behavior without requiring a database server.

Runtime Assembly

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.

Why Providers Matter

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 Rust: From Java Feature Parity to Multi-database Runtime

· 3 min read
TeaQL Code Gen
Core Contributor

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.

What Carries Over from Java

The useful Java ideas are not Spring-specific. They are TeaQL-specific:

  • generated Q APIs;
  • readable query builders;
  • field and relation selectors;
  • SmartList style result metadata;
  • runtime context;
  • checker infrastructure;
  • translated validation messages;
  • mutation events;
  • graph writes;
  • relation enhancement;
  • relation aggregate enhancement;
  • safe value access.

Rust implements those ideas with Rust types, traits, crates, and provider registration.

What Does Not Carry Over Directly

Some Java features should not be copied blindly:

  • Spring Boot autoconfiguration;
  • Java GraphQL generation;
  • Java BaseService/controller conventions;
  • reflection-style runtime mechanisms;
  • the full Java database dialect matrix.

Rust needs a Rust-native shape. The runtime should be explicit, crate-based, and provider-backed.

The Rust Workspace

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)];
  • provider crates for SQLx PostgreSQL, SQLx MySQL, SQLx SQLite, and rusqlite SQLite.

This keeps the core runtime separate from database adapters.

Generated Rust Crates

teaql-code-gen can emit Rust service crates. A generated crate can include:

  • entity structs;
  • Q facade;
  • generated request builders;
  • behavior skeletons;
  • checker skeletons;
  • runtime module registration;
  • repository and behavior registries;
  • provider-backed runtime helpers.

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?;

Multi-database Runtime

The Rust direction is provider-based:

  • SQLx PostgreSQL for production-grade PostgreSQL backends;
  • SQLx MySQL for common enterprise MySQL systems;
  • SQLx SQLite for local-first apps, tests, and small services;
  • rusqlite SQLite for embedded and multi-architecture deployments;
  • MemoryRepository for no-database tests and demos.

The generated API should stay stable while the provider changes below it.

Current State

The Rust runtime is already useful for core generated-domain paths:

  • typed queries;
  • relation loading;
  • grouped aggregates;
  • graph writes;
  • checkers;
  • events;
  • schema bootstrap;
  • SQL debug logs;
  • generated high-level 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.

Why TeaQL Supports Both SQLx and rusqlite

· 2 min read
TeaQL Code Gen
Core Contributor

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: Async Backend Services

SQLx is a strong fit for async server-side Rust services.

TeaQL uses SQLx providers for:

  • PostgreSQL;
  • MySQL;
  • SQLite.

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:

  • API servers;
  • backend services;
  • PostgreSQL business systems;
  • MySQL enterprise systems;
  • local-first services that still use async SQLite.

rusqlite: Embedded SQLite

rusqlite is a strong fit for synchronous embedded SQLite.

That matters for deployments where the database is not a remote backend service:

  • routers;
  • edge devices;
  • appliance controllers;
  • offline local tools;
  • multi-architecture devices;
  • small embedded runtimes;
  • local agent memory.

In those cases, an async pool-oriented provider may be unnecessary weight. A synchronous SQLite provider can be easier to deploy and reason about.

Same Business API, Different Provider

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.

Provider Boundary

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.

Choosing Between Them

Use SQLx when:

  • the app is async;
  • the database is part of a backend service;
  • pooling and async transactions matter;
  • the provider is PostgreSQL or MySQL;
  • SQLite is used in an async service/test path.

Use rusqlite when:

  • SQLite is embedded;
  • sync execution is simpler;
  • deployment footprint matters;
  • the target is edge, device, or local runtime;
  • the app should avoid an async database stack.

Why This Matters for TeaQL

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.

Building a DDD Data Runtime with Generated Typed Queries in Rust

· 8 min read
TeaQL Code Gen
Core Contributor

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.

Why Generate the API?

The original pressure came from systems where the same entity model needed to support several kinds of behavior:

  • ordinary list and detail queries;
  • nested relation loading, including paths such as merchant.platform or platform.merchant_list;
  • graph writes where a parent object and children are committed together;
  • additive schema bootstrap for development and tests;
  • checker and validation logic that can inspect and fix entities;
  • simple and grouped statistics;
  • JSON serialization and JSON-expression style search.

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.

Runtime Pieces

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.

Query Construction

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.

Graph Writes

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:

  • is this child new, already present, a reference, or explicitly removed?
  • should missing children be soft-deleted or left alone?
  • should a relation write attach a foreign key, or is it detached?
  • if a reference points at a deleted row or the wrong version, should the graph write fail?

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.

Schema Bootstrap

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.

Checkers and Domain Validation

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.

Statistics

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.

Tradeoffs

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.

What Works Today

The current Rust runtime and generated crate tests cover:

  • SQLite schema bootstrap and additive column changes;
  • PostgreSQL schema bootstrap with SQLx;
  • CRUD, optimistic locking, soft delete, and recover;
  • typed entity fetch into SmartList<T>;
  • nested relation enhancement;
  • complex object commit through graph writes;
  • transaction rollback for graph writes;
  • generated Q APIs against SQLite;
  • typed checker adapters from generated crates;
  • JSON serialization and JSON-expression search paths;
  • simple aggregates, grouped aggregates, and relation aggregate statistics.

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.

What Is Not Done

The biggest gaps are:

  • more complete memory repository parity for relation enhancement and subquery execution;
  • richer checker semantics, especially nested typed object locations and domain-specific labels;
  • richer event payloads with old/new values and typed snapshots;
  • more value types such as UUID and bytes;
  • a decision on whether Rust needs a higher-level service layer above the repository/runtime APIs.

Those gaps are real. They are better kept visible than hidden behind a larger feature list.

Why This Shape Is Worth Exploring

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 Rust Core Runtime

· One min read
TeaQL Code Gen
Core Contributor

TeaQL expands into Rust. The core runtime ships with procedural macros, dual database support, and a unified id space.

Why Rust

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.

Procedural Macros

#[derive(TeaQLEntity)]
struct User {
id: u64,
name: String,
}

The TeaQLEntity macro derives:

  • Schema mapping
  • Query DSL methods
  • Graph relation helpers

Database Support

DatabaseSchema EnsuringStatus
SQLiteensure_schemaReady
PostgreSQLensure_schemaReady
let db = SqliteBackend::open("app.db").await?;
db.ensure_schema::<User>().await?;

u64 Id Space

All entity ids moved from i32 to u64:

  • 64-bit range eliminates overflow concerns
  • Consistent with Snowflake-style distributed ids
  • Progress tracking built into the runtime

What's Next

Graph writes and a query DSL are already in progress.

Generated View Runtime

· 2 min read
TeaQL Code Gen
Core Contributor

Historical note: This article documents an earlier TeaQL generation target. Early versions emitted backend templates, UI pages, mobile clients, and service scaffolding directly into application workspaces. Modern TeaQL has moved to a clearer boundary: generation produces versioned libraries, deterministic business APIs, query DSLs, and runtime capabilities that applications consume as artifacts. This is more friendly to AI-assisted coding and DevOps because AI agents can focus on business workflows, integrations, tests, and product behavior, while generated capabilities are reviewed, tested, published, upgraded, and rolled back through normal dependency and release pipelines.

In 2019 the generated frontend and backend started to look more like a runtime, not a collection of pages.

The diff shows work around java_view_base_view_page.jsp, generated checker templates, generated scripts, event processors, base constants, locale services, React object base components, dashboards, tables, profile pages, permission pages, and step forms.

Base View Pages

The generated Java view layer introduced reusable base view concepts:

  • object view pages
  • list-of view pages
  • view models
  • profile and preference views
  • permission-aware presentation

This helped separate generated page behavior from individual object templates. Repeated page mechanics moved into base templates, while object-specific code stayed small.

Generated Validation Surface

Checker generation also became more visible:

  • base checker templates
  • object checker templates
  • checker manager templates
  • update parameter checks
  • reference parameter update checks

Validation was moving closer to generated domain behavior. Instead of relying on handwritten service guards, the generator could emit repeatable checks from model metadata.

Events and Step Forms

Event and step-form templates appeared beside the normal CRUD forms.

That mattered because real business apps rarely stop at simple create/update pages. The generator began to support process-shaped UI and backend hooks, such as change events, staged forms, and generated action wiring.

Locale and Presentation Polish

The React templates also gained more presentation behavior:

  • locale lookup
  • permission-based action display
  • generated operation columns
  • table sorter state
  • dashboard and profile variants

These were not model additions. They were generated application capabilities that made the output closer to something teams could use directly.