Skip to main content

TeaQL 1.0.0 Release Notes

· 3 min read
TeaQL Code Gen
Core Contributor

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.

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.

Generated APIs for domain-driven development

· 2 min read
TeaQL Code Gen
Core Contributor

Domain-driven development breaks down when the domain language disappears from the code path.

That happens easily in data-heavy systems. The model may be discussed in design sessions, but implementation code becomes SQL strings, mapper files, repository methods, DTOs, and service glue.

TeaQL uses generated APIs to keep the domain model visible.

Model First

TeaQL starts from a domain model:

  • entities;
  • scalar fields;
  • object relations;
  • child lists;
  • constants;
  • validation expectations;
  • data-service targets.

The generator turns that model into APIs. Application code then works with the generated vocabulary instead of reconstructing it by hand.

Queries as Domain Language

Generated query methods make intent explicit:

Q.orders()
.filterByMerchant(ctx.getMerchant())
.selectCustomer(Q.customers().selectName())
.selectLineItemList(Q.lineItems().selectSku().selectQuantity())
.countLineItems()
.executeForList(ctx);

The code names the domain structure:

  • orders;
  • merchant;
  • customer;
  • line items;
  • SKU;
  • quantity.

That is more reviewable than a service method that hides several mapper calls and response transformations.

Relations Matter

DDD systems are rarely flat. Relations carry meaning:

  • an order has line items;
  • a merchant belongs to a platform;
  • an employee belongs to a department;
  • a ticket has comments and assignments.

TeaQL relation loading APIs give those relationships a generated shape. The runtime can still decide how to execute the load efficiently.

Graph Writes

Domain changes often arrive as object graphs:

  • create an order with lines;
  • update a parent and merge children;
  • remove a child row;
  • attach an existing reference;
  • keep missing children for a relation that should not delete absent rows.

TeaQL Rust models this through graph nodes, graph operation state, mutation planning, and transaction boundaries. Java TeaQL carries the same high-level idea through entity save and graph-oriented runtime behavior.

Validation and Runtime Policy

Domain logic is not only data shape. It also includes:

  • required fields;
  • status transitions;
  • tenant rules;
  • permission policy;
  • audit metadata;
  • language-aware validation messages.

TeaQL keeps those concerns near runtime context and generated model hooks instead of scattering them across controllers.

Why Generate?

Handwritten domain APIs are possible. The problem is consistency.

Generation gives every entity the same baseline:

  • field selectors;
  • filters;
  • relation selectors;
  • aggregate helpers;
  • request builders;
  • checker hooks;
  • behavior hooks;
  • runtime registration.

Teams can then focus on the parts of the domain that are actually special.

The Short Version

Generated APIs are not a shortcut around domain modeling.

They are a way to make the domain model executable, reviewable, and reusable across application code, runtime providers, and AI coding tools.

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.

TeaQL vs MyBatis: an order page example

· 3 min read
TeaQL Code Gen
Core Contributor

MyBatis is a productive and familiar tool for Java teams. It gives developers direct control over SQL and mapping. For many simple services, that is enough.

The pressure appears when a business page needs more than one table and more than one query shape.

Consider an order page.

The Order Page Shape

A typical order page may need:

  • the order list;
  • customer name;
  • order status;
  • line item preview;
  • line item count;
  • status counts for tabs;
  • pagination;
  • merchant or tenant filtering;
  • permission-aware visibility.

With mapper-oriented persistence, this often becomes several mapper methods, XML fragments, DTO assembly, and post-query stitching.

The MyBatis Shape

A MyBatis implementation might involve:

  • one SQL query for the order list;
  • one query or join for customer fields;
  • one query for line item previews;
  • one query for counts;
  • one query for grouped status statistics;
  • result maps or manual DTO mapping;
  • service code to assemble the final response.

That is explicit and controllable, but the business intent is spread across files.

The TeaQL Shape

TeaQL tries to express the page as one generated business request:

User userOrderInfo = Q.users()
.filterWithId(userId)
.countOrder()
.facetByOrderStatus("statusWithCount", Q.orderStatus().countOrders())
.selectOrderList(
Q.ordersWithId()
.selectOrderId()
.selectDate()
.offset(0, 10)
.selectLineItemList(
Q.lineItemsWithId().selectImageURL().limit(3)
)
.countLineItems()
)
.execute(context);

The API is still explicit, but the explicitness is at the business level:

  • user;
  • orders;
  • status counts;
  • order list;
  • line items;
  • pagination;
  • line item count.

What TeaQL Reduces

TeaQL does not eliminate thinking. It reduces repetitive plumbing:

  • mapper XML;
  • duplicated DTO assembly;
  • repeated relation-loading code;
  • repeated count/statistics query fragments;
  • stringly-typed field references;
  • one-off page query services.

The generated model becomes the shared vocabulary.

Where MyBatis Still Wins

MyBatis remains a good choice when:

  • a query must be hand-tuned;
  • SQL is the clearest expression of the business requirement;
  • the domain model is small;
  • teams need exact control over vendor-specific SQL;
  • generated APIs would add weight without reducing repetition.

TeaQL and MyBatis do not need to be ideological opposites. TeaQL is valuable when the business model is large enough that generated APIs reduce repeated work.

The Review Difference

In a TeaQL code review, the reviewer can ask:

  • does this page select the correct relations?
  • are the counts grouped correctly?
  • is pagination applied at the right level?
  • does the runtime context apply tenant and permission policy?

That is a higher-level review than checking XML fragments and DTO stitching.

The Short Version

MyBatis gives direct SQL control. TeaQL gives generated business APIs.

For complex business pages, TeaQL keeps page intent closer to the domain model.

What is TeaQL?

· 2 min read
TeaQL Code Gen
Core Contributor

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.

The Core Idea

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.

What TeaQL Generates

Depending on the target stack, TeaQL can generate:

  • entity types;
  • query builders;
  • safe expression helpers;
  • relation loading helpers;
  • checker and behavior skeletons;
  • graph save entrypoints;
  • runtime module registration;
  • agent guidance for generated APIs.

In Java, this appears as fluent generated request APIs. In Rust, generated crates expose Q::entities() style query builders over teaql-rs.

Why It Matters

Large business systems repeat the same concepts across many screens and services:

  • users and roles;
  • orders and line items;
  • customers and accounts;
  • status counts and dashboards;
  • parent-child graph saves;
  • tenant, permission, audit, and localization policies.

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 and Rust

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.

A Small Example

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 in One Sentence

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.

Why AI-generated software needs deterministic business APIs

· 2 min read
TeaQL Code Gen
Core Contributor

AI coding tools can produce useful application code quickly. The problem is not speed. The problem is boundary control.

If an AI tool has to infer persistence behavior from scattered SQL, repositories, mapper XML, DTOs, and service conventions, it will eventually guess wrong.

TeaQL exists to make that boundary deterministic.

The Guessing Problem

When AI writes data-access code directly, it must infer:

  • table names;
  • column names;
  • relation cardinality;
  • tenant filters;
  • permission rules;
  • deleted-row semantics;
  • transaction boundaries;
  • pagination rules;
  • response shape;
  • database dialect differences.

Some of these guesses can pass a simple test and still be wrong in production.

The Business API Boundary

TeaQL generates APIs from the domain model. That means AI code can compose with named business methods instead of inventing storage behavior.

let orders = Q::orders()
.select_customer_with(Q::customers().select_name())
.select_line_item_list_with(Q::line_items().select_sku())
.which_statuses_are("PAID")
.page(1, 20)
.execute_for_list(&ctx)
.await?;

The model controls what fields and relations exist. The runtime controls how the query executes. The AI composes within those boundaries.

Deterministic Does Not Mean Rigid

A deterministic API can still be expressive:

  • filters can be combined;
  • relation loads can be nested;
  • aggregates can be grouped;
  • graph writes can save parent and child objects together;
  • provider-specific behavior can be selected below the API.

The point is that the API surface is stable and reviewable.

Better Prompts

TeaQL also makes prompts smaller.

Instead of giving an AI tool the entire schema, SQL examples, repository conventions, and DTO rules, a team can provide a generated API guide:

Use TeaQL Q APIs for reads.
Use generated relation selectors.
Execute through UserContext.
Do not write raw SQL unless explicitly requested.
Use graph save for parent-child persistence.

That is a stronger contract than a long explanation of database structure.

Runtime Safety

The generated API is only half the story. Runtime boundaries matter too.

TeaQL keeps execution behind context and provider layers:

  • Java uses UserContext as the runtime boundary.
  • Rust uses teaql_runtime::UserContext, repository registries, behavior hooks, and provider registration.
  • Database providers execute against PostgreSQL, MySQL, SQLite, rusqlite, or memory.

AI-generated application code should not own those decisions.

The Short Version

AI tools are fast at composition. They are unreliable at reconstructing business rules from infrastructure code.

TeaQL gives them deterministic business APIs to compose.

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.

Generated Query APIs vs Handwritten SQL

· 5 min read
TeaQL Code Gen
Core Contributor

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.

The Handwritten SQL Case

There are many cases where handwritten SQL is the right tool:

  • a query is performance-critical and carefully tuned;
  • the shape is reporting-specific rather than domain-object-specific;
  • the team wants exact control over joins, hints, indexes, and execution plans;
  • the service is small enough that generated APIs would add more weight than value.

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.

The Generated API Case

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:

  • fields are exposed as methods;
  • object relations are selected by relation name, not storage column name;
  • readable filters follow the model vocabulary;
  • subqueries can be constructed from the target entity's own generated API;
  • execution returns typed entities or typed SmartList<T> collections.

That matters when the model is large and the same entity relationships appear across many services, pages, and business workflows.

Relation Semantics

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.

Subqueries Without Stringly-Typed Entity Names

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.

Statistics

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.

Why Keep a Generic Runtime?

If generated APIs are useful, why not make the whole runtime typed?

Because TeaQL still needs dynamic behavior:

  • generated service crates vary by domain model;
  • aggregate rows can contain dynamic projection fields;
  • JSON-expression search and raw SQL escape hatches need flexible query metadata;
  • graph planning operates across entity types;
  • relation enhancement starts with records and then maps into typed entities.

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.

Where Generated APIs Hurt

Generated code is not free:

  • it increases compile time;
  • it creates large diffs when templates change;
  • it can hide SQL details if developers treat it as magic;
  • it needs strong generated-crate tests to catch template regressions.

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.

A Practical Boundary

The boundary I use is this:

  • if the query expresses a domain object shape, relation graph, validation path, or repeated business filter, prefer the generated Q API;
  • if the query expresses a one-off report, hand-tuned performance path, or database-specific operation, prefer handwritten SQL;
  • if generated code starts hiding a critical execution detail, drop down a level.

TeaQL's design only works if the lower level remains available.

The Real Goal

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.

Saving Object Graphs Is Not Just Insert

· 5 min read
TeaQL Code Gen
Core Contributor

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 Easy Case

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.

Identity and State

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.

Relation Ownership

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.

Missing Children

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:

  • the caller did not load that child and is not trying to change it;
  • the caller loaded the full relation and intentionally removed that child;
  • the relation is a projection and should not imply ownership;
  • the child should be soft-deleted rather than physically deleted.

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.

Planning Before Execution

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.

Transactions Are Part of the Feature

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.

Typed Entity Graphs

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.

What This Is Not

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.

Why It Matters

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.

Open Source Cleanup

· One min read
TeaQL Code Gen
Core Contributor

TeaQL prepares for broader open source adoption. Codebase cleaned, repositories split, and AI agent guidance added.

Repository Module Split

teaql-rs/
teaql-core/
teaql-sql/
teaql-runtime/
teaql-macros/
teaql-provider-sqlx-postgres/
teaql-provider-sqlx-sqlite/
teaql-provider-sqlx-mysql/
teaql-provider-rusqlite/

Each module is independently versioned and testable. The workspace Cargo.toml ties them together for local development.

Agent Guidance Generation

TeaQL Code Gen now produces agent guidance files:

# Agent Guidance: User Service

## Entities
- User: id, name, email, status
- Order: id, user_id, total, status

## Relations
- User has many Order

## Common Queries
- List active users
- Sum orders by status

These files help AI agents understand the domain model without reading source code.

StackGuidance Format
JavaMarkdown + annotations
RustMarkdown + doc comments

Cleanup Checklist

  • Removed internal URLs and credentials
  • Added LICENSE (Apache-2.0)
  • Added CONTRIBUTING.md
  • CI workflows for each crate

Merge

Pull request #76 merged the save graph enhancements. The branch history is now clean and linear.

What's Next

Documentation site refresh and community onboarding.

TeaQL Rust v0.7.0 Release

· One min read

TeaQL Rust reaches v0.7.0. The crates are published and documented.

Crate Structure

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.

Aggregation Cache

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.

SQL Debug Logging

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.

Extended Query Expressions

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.

v0.7.1 Patch

Quick patch for documentation links. No functional changes.

What's Next

Repository module split and open source cleanup.

Multi-Stack Code Generation

· One min read
TeaQL Code Gen
Core Contributor

TeaQL Code Gen learned Rust. One model definition now produces both Java and Rust artifacts.

Separate Stack Templates

templates/
java/
entity.java.vm
repository.java.vm
rust/
entity.rs.vm
query.rs.vm

Each stack has isolated templates. No cross-language leakage.

Rust Lib Generators

// Generated from the same .teaql model
pub struct User {
pub id: u64,
pub name: String,
}

impl TeaQLEntity for User {
const TABLE: &str = "user_t";
}

Typed Query Return Types

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.

Explicit Aggregation APIs

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.

Relation Generation

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.

Graph Writes and Query DSL

· One min read
TeaQL Code Gen
Core Contributor

The Rust runtime now supports typed graph mutations and a fluent query DSL.

Typed Entity Graph Saves

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.

SQL Id Space Generator

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.

Decimal for Aggregates

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.

Query DSL

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.

Checker Events

Graph mutations emit checker events:

  • Pre-save validation
  • Post-save side effects
  • Rollback on failure

This hooks into the transactional planner for complex business rules.

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.

Optional Redis and Property-Level RawSQL

· One min read
TeaQL Code Gen
Core Contributor

Optional Redis significantly lowers deployment barriers. Property-level RawSQL increases query flexibility.

Optional Redis

+ Redis is now an optional dependency
+ LocalLockService: in-memory lock for non-Redis environments
+ RedisLockService: distributed lock when Redis is available
+ TQLAutoConfiguration: conditionally loads based on classpath

Before: spring-boot-starter-data-redis was mandatory.

After: Works without Redis, automatically falling back to local locks.

Property-Level RawSQL

@RawSQL("CONCAT(first_name, ' ', last_name)")
String getFullName();

Three levels fully supported: Property-level, Criteria-level, Select-level.

Aggregation Function Prefixes

+ Configurable prefix for aggregation result columns
+ Prevents column name conflicts in multi-aggregation queries

Request Pagination

Generated Request classes automatically include page and pageSize fields.

Expression orElse/orElseThrow

String name = Q.user().name().orElse("Unknown");
String email = Q.user().email()
.orElseThrow(() -> new BusinessException("Email required"));

Aligns with Java Optional conventions.

Internationalization and SQLite Enhancements

· One min read
TeaQL Code Gen
Core Contributor

Internationalization milestone with SQLite schema migration enhancements.

Traditional Chinese Support

+ Full Traditional Chinese (zh-TW) translation
+ BaseLanguageTranslator base class

Supported languages: English (en), Simplified Chinese (zh-CN), Traditional Chinese (zh-TW).

SQLite ALTER COLUMN

SQLite's limited ALTER TABLE support implemented via table recreation:

+ SQLiteRepository: alter column via table recreation
+ Data preservation during table recreation

CODE_AS_IS Constant Strategy

Constant values emitted exactly as defined in the domain model without transformation.

Boolean Property Naming

Default boolean naming changed from hasXxx to haveXxx:

// Before
user.hasPermission()

// After (default)
user.havePermission()

Legacy projects can set use_has='true' to maintain compatibility.

MySQL Foreign Keys and Schema Management

· One min read

Important MySQL schema management improvements.

Auto Foreign Key Generation

ensureTables automatically creates foreign key constraints:

+ MysqlRepository: FK generation logic
+ Auto-create FOREIGN KEY based on domain model relationships

No need to manually manage foreign key relationships.

ALTER TABLE Fix

Corrected MySQL ALTER TABLE statement generation:

+ Column type changes and nullability updates
+ Schema migration edge case handling

ensureTables Flow

  1. Create missing tables
  2. Add missing columns
  3. Generate foreign keys (new)
  4. Fix column type mismatches

Checker Optimization

needCheck logic optimized to reduce unnecessary validation overhead.

Auto Indexing and Context Injection

· One min read
TeaQL Code Gen
Core Contributor

Auto-indexing, context injection, and query optimization improvements.

Auto Index Creation

+ Auto-create indexes for commonly filtered fields
+ Configurable index strategies per entity

ensureTables now generates CREATE INDEX statements automatically.

UserContext Re-Injection

// Reinject a new resolver at runtime
ctx.reinjectResolver(newTenantResolver);

Useful for multi-tenancy, data source switching, and testing.

Checker Type Validation

The validation framework now validates field type matching, catching type mismatch errors at the application layer.

Sub-Query Mixing

SQLRepository supports canMixinSubQuery, allowing sub-queries to be mixed into the main query as JOINs:

+ Sub-queries mixed into main query as JOINs
+ Reduced database round-trips

Minimal Field Merge

When generating update requests, only the minimal set of changed fields is merged, producing more efficient SQL.

RawSQL Queries and Model Visualization

· One min read
TeaQL Code Gen
Core Contributor

Full RawSQL support plus a suite of model visualization tools.

RawSQL Three-Level Support

// WHERE clause
Q.orders().filter(
RawSqlCriteria.of("EXTRACT(YEAR FROM create_time) = 2025")
).executeForList(ctx);
+ RawSQL in search criteria (WHERE)
+ RawSQL in select projections
+ RawSQL in criteria expressions

Data Design Documentation Generator

Generates Markdown documentation from domain models, including ER diagrams, field descriptions, and relationship docs.

HTML Model Browser

A searchable, interactive domain model browser:

+ HTML model viewer: interactive domain model browser
+ Searchable entity list
+ Relationship visualization
+ Table relation count display

Word Cloud Visualization

Domain model word cloud with concept sizing based on entity importance.

Other Improvements

  • Optimized error reporting (line numbers, original SQL, parameter values)
  • startsWith / endsWith naming fix
  • gt/lt operators restricted to numeric types

Redisson Migration and Distributed Locks

· One min read
TeaQL Code Gen
Core Contributor

Migrated from Spring Data Redis to Redisson for a richer Redis client experience.

Redisson Migration

- spring-data-redis RedisTemplate
+ Redisson: RMap, RSet, RLock, etc.

Redisson provides richer distributed data structures, built-in distributed locks, more efficient serialization, and connection pooling.

Lock Abstraction

// Local lock (single JVM)
lockService.acquireLocal("order-process", () -> {
// critical section
});

// Distributed lock (multi-instance)
lockService.acquireDistributed("order-process", () -> {
// critical section across all instances
});
+ LockService interface
+ LocalLockService: ReentrantLock-based
+ DistributedLockService: Redisson-based
+ Configurable lock timeout and retry

Blob Objects and Optimistic Locking Improvements

· One min read
TeaQL Code Gen
Core Contributor

Blob object type and optimistic locking mechanism improvements.

BlobObject Type

Store large binary data (images, documents, files):

@Entity
public class Document {
private String name;
private BlobObject content; // stores file data
}
+ BlobObject: binary data storage type
+ Integrated with SQLRepository for persistence
+ Streaming support for large files

Actual Change Detection

Version number is only incremented when properties have actually changed, avoiding meaningless version bumps.

Multi-Entity Version Conflict Fix

When updating multiple entities in the same transaction, each entity's version is tracked independently.

Aggregation Query Caching and Local Deployment

· One min read

Aggregation query caching significantly improves dashboard performance. Local deployment mode is now supported.

Aggregation Query Cache

+ AggregationCache: caches aggregate query results
+ Recursive cache invalidation when entities change
+ Configurable cache TTL

Significantly improves performance for dashboards and reports with repeated aggregation queries.

Equals/HashCode

ID-based equals() and hashCode() implementations:

Set<Order> uniqueOrders = new HashSet<>(orderList);

Local Deployment Support

TeaQL can now run without cloud infrastructure for development and testing environments.

Global Cache Capacity Limits

Prevent memory exhaustion in long-running applications.

Task Runner Framework and Batch Operations

· One min read
TeaQL Code Gen
Core Contributor

Async task framework, batch operations, and entity update API improvements.

TaskRunner Framework

+ TaskRunner: execute tasks within TeaQL context
+ Support for async and scheduled tasks
+ Integration with permission and data scoping

Entity-Level Update API

Q.orders().updateOnEntity(order, ctx);

Pre-Action Validation

+ validateForSave(): runs before persistence
+ validateForDelete(): runs before deletion
+ Custom validation logic per entity type

Excel Export

XLSX block support in the view rendering system for data export.

Batch Operations

The code generator now produces batch operation templates for efficient bulk processing.

Other Improvements

  • Compact request/response logging
  • BaseService error handling enhancements
  • China administrative region data support

DuckDB Integration and Request Logging

· One min read
TeaQL Code Gen
Core Contributor

DuckDB embedded analytical database integration, plus comprehensive request logging infrastructure.

DuckDB Integration

+ teaql-duck: DuckDB repository implementation
+ Embedded analytical database support

DuckDB is ideal for OLAP analytical workloads.

Sub-View Support

Views now support nested sub-views:

+ Sub-view fields in templates
+ Sub-view action candidates
+ Empty view customization per context
+ Go-to-view navigation

Request Logging and Tracing

+ Request body caching for logging
+ Response header debug logging
+ Client IP tracking in UserContext
+ MDC trace headers (trace_id, span_id)
+ Response body logging

Toast Messages

Toast/notification message support in the view rendering system.

UserContext Edit Hook

// Modify requests before execution
ctx.beforeExecuteRequest(request -> {
// Add default filters
// Audit logging
// Permission checks
});

View Renderer with Redis Caching

· One min read
TeaQL Code Gen
Core Contributor

The view rendering system gained Redis-backed persistent caching, and the Service Request framework landed.

Redis View Cache

UserContext now includes DataStore/Redis DataStore:

+ Redis-backed view data storage
+ View parser with template rendering
+ Customizable empty view support

Service Request Framework

+ Service request controller registry
+ Processor-based request handling
+ Request path prefix configuration

UI Template Renderer

Template renderer for generating UI views from domain models using ObjectMapper for JSON serialization.

Generator Updates

  • Service request controller/processor generation
  • Request path prefix configuration
  • Candidate preparation generation
  • viewObject and JsonMe support

GraphQL Queries and BaseService CRUD Framework

· One min read
TeaQL Code Gen
Core Contributor

Kicking off 2024 with GraphQL support, service layer framework, and view translation.

GraphQL Support

query {
orders(filter: {status: {eq: "ACTIVE"}}) {
id
amount
customer { name }
}
}
+ teaql-graphql: GraphQL query support
+ Dynamic attribute support
+ Scalar/JSON type handling
+ Relation property selection
+ Simple property optimization

BaseService Framework

@RestController
public class OrderService extends BaseService<Order, CustomUserContext> {
// Inherits save, delete, find, update, etc.
}
+ BaseService: auto-generated CRUD operations
+ Register controller: auto-exposes REST endpoints

View Translation System

  • NoopTranslator: Pass-through for development
  • SimpleChineseViewTranslator: Chinese localization
  • Action translation for internationalized UIs

HANA Entity Descriptors

SAP HANA-specific entity, property, and relation descriptors.

Stream API and Aggregation Functions

· One min read
TeaQL Code Gen
Core Contributor

Year-end update: streaming queries, Spring Boot upgrade, and aggregation library expansion.

Stream API Support

Q.orders()
.filter(Q.orders().status().eq("ACTIVE"))
.stream(ctx)
.forEach(order -> process(order));
+ Stream<T> stream(UserContext ctx)
+ Streaming ResultSet processing

Spring Boot 3.2.0 Upgrade

  • Faster startup time
  • Better GraalVM native image support
  • Updated dependency versions

Expanded Aggregation Functions

+ stddev (standard deviation)
+ variance

SoundsLike Operator

Fuzzy text matching for name searches:

Q.users().filter(Q.users().name().soundsLike("John")).executeForList(ctx);

Other Improvements

  • toList() / toSet() conversions
  • Response header support
  • Result set processing pipeline refactor

Snowflake Distributed ID Generator

· One min read
TeaQL Code Gen
Core Contributor

The teaql-snowflake module brings distributed unique ID generation.

Snowflake IDs

+ teaql-snowflake: distributed unique ID generation
+ 64-bit IDs: timestamp + worker node + sequence

Features:

  • Globally unique without coordination
  • Time-ordered for index-friendly storage
  • Configurable worker node IDs

Enhance Children

Automatically load and enrich child collections when querying parent entities:

+ Auto-enrich child collections
+ Recursive enhancement support
+ Fix: enhanced children handle update operations correctly

MSSQL ALTER TABLE

  • ALTER TABLE column support
  • ADD COLUMN SQL generation
  • Order by ID when paging without explicit sort

Multi-Database Support: Oracle, DB2, HANA, MSSQL

· One min read
TeaQL Code Gen
Core Contributor

TeaQL gained four new database backends in a single sprint.

New Databases

+ teaql-oracle: Oracle database support
+ teaql-db2: IBM DB2 support
+ teaql-hana: SAP HANA support
+ teaql-mssql: Microsoft SQL Server support

Each module includes:

  • Database-specific DDL generation (ensureTables)
  • Java ↔ SQL type mapping
  • Custom SQL syntax handling

JdbcTemplate Migration

Migrated from raw JDBC to Spring JdbcTemplate for better resource management, connection pooling, and exception handling.

MySQL Improvements

  • tinyintBoolean mapping
  • intInteger mapping
  • timestampLocalDateTime mapping
  • GBK charset ORDER BY sorting fix
  • Semicolon handling fix

Oracle Customizations

  • Primary table INNER JOIN, auxiliary table LEFT JOIN
  • Partition fix
  • Column label handling

Pagination

Native pagination with page and pageSize:

Q.orders()
.filter(Q.orders().status().eq("ACTIVE"))
.page(1, 20)
.executeForList(ctx);

Core Architecture Refactor: Modularization and Reactive Support

· One min read
TeaQL Code Gen
Core Contributor

TeaQL core was decoupled from Spring, the SQL repository became its own module, and WebFlux reactive support was added.

Module Split

Before

teaql (monolithic)
├── Entity/Request/Context
├── SQLRepository (all databases)
└── Spring auto-config

After

teaql (core, no Spring dependency)
├── Entity/Request/Context
├── Expression framework
└── Checker

teaql-sql (SQL repository base)
teaql-mysql / teaql-pg / teaql-oracle (database-specific)
teaql-autoconfigure (Spring Boot starter)

WebFlux Support

+ WebFlux reactive endpoint support
- Removed spring-boot-starter-web dependency from core

TeaQL now works with both Spring MVC and Spring WebFlux.

JDBC DataSource

Added JdbcDataSource for explicit JDBC connection management.

Dynamic Search and Expression System

· One min read
TeaQL Code Gen
Core Contributor

Over 60 commits in a single month introduced dynamic search, the expression API, event system, and web framework.

DynamicSearchHelper

Build search queries from external input (JSON, request parameters) without writing Java code:

DynamicSearchHelper.search(ctx, "Order", jsonFilter, pageable);

Ideal for generic admin panels, mobile backends, and API gateways.

ValueExpression API

Reference entity properties in a type-safe way:

Q.orders().filter(
ValueExpression.of(Q.orders().amount()).gt(100)
).executeForList(ctx);

ID Generator System

  • Remote ID Generator: Fetch IDs from a remote service
  • Default ID Generator: Local UUID-based fallback
  • Configurable per-entity ID generation strategy

Property Change Events

PropertyChangeEvent tracks which properties changed during an entity update, enabling selective SQL UPDATE and audit logging.

Web Response Framework

  • WebStyle: UI style definitions
  • WebAction: Frontend action descriptors
  • WebResponse: Standardized response objects

Soft Delete

entity.setDeleted(true);  // soft delete
entity.recover(); // restore

Code Generator Goes Production with Validation Framework

· One min read
TeaQL Code Gen
Core Contributor

The code generator evolved from a prototype into a production-ready Spring Boot generation engine.

Code Generator

Spring Boot Generation Engine

  • 26 files changed, 467 lines added
  • Templates migrated to Spring Boot STG (StringTemplate)
  • Support for lib and ZIP file imports
  • Auto-generated UserContext

Domain Model Reuse

DomainParser now supports importing domain models from local filesystem paths, ZIP archives, and remote library references.

Runtime Enhancements

Checker Validation Framework

Automatically validates domain objects before persistence to ensure data integrity:

+ SQLRepositorySchemaHelper: added checker validation (66 lines)
+ Checker: validates entity constraints before save/update

EntityDescriptor Metadata

A metadata object describing the full structure of an entity, including children, properties, and relationships.

Other Improvements

  • TypeCriteriaParser: Filter entities by type hierarchy
  • Smart IN Optimization: Single-value IN automatically becomes =, NOT IN becomes !=
  • SmartList: Supports get(index) and size()
  • GitHub Actions CI/CD: Automated publishing to GitHub Packages

Rebranding: From DoubleChain to TeaQL

· One min read

TeaQL officially broke away from its legacy brand to become an independent open-source project.

Package Restructuring

- com.doublechaintech.data
+ io.teaql.data

All classes were migrated, including BaseEntity, BaseRequest, SQLRepository, UserContext, and the entire expression parser framework.

This migration marked the birth of TeaQL as a standalone brand.

From web-code-generator to TeaQL

· 4 min read
TeaQL Code Gen
Core Contributor

TeaQL did not appear from nowhere, and it is not simply a rename of an old generator.

It came from a long engineering lineage around one persistent problem: business software repeats the same structures across projects. Domain models, relationships, permissions, queries, validation, workflows, persistence rules, and presentation metadata appear again and again. The hard question is not whether code can be generated. The hard question is where the generation boundary should live.

Before the Git History

The earliest internal version of this direction was built in 2003.

That first version was already focused on reducing repetitive business application code in enterprise systems. The visible Git history of web-code-generator starts on April 1, 2016, but that commit should be read as a preserved snapshot of an older internal lineage, not as the beginning of the idea.

This history matters because the core problem stayed consistent for more than two decades. What changed was the shape of the generated output.

The Workspace Generation Era

The early web-code-generator approach generated source code directly into application workspaces.

That made sense for the development model of its time. A team could describe domain objects and generate a working application surface:

  • Java POJOs, DAO interfaces, JDBC mappers, SQL, and manager services
  • JSP and later React DVA admin pages
  • Android, Swift, and miniapp client templates
  • generated forms, tables, dashboards, search pages, and locale resources
  • IAM, validation, event, and service scaffolding

This proved that model-driven generation could cover much more than CRUD. It could generate a large part of the repetitive structure around real business systems.

But generating source code into the developer workspace also has a cost. Generated files become mixed with handwritten files. Reviews get noisy. Upgrades create large diffs. AI coding agents must search through many repeated files before finding the business logic that actually matters. DevOps teams have to govern generated behavior indirectly through each application repository.

The Transition After 2022

After 2022, web-code-generator became less of a standalone product story and more of the engineering transition path into TeaQL.

Some important work still happened in the old repository during this overlap period:

  • Request DSL improvements
  • JSON search and list search
  • aggregation, count, sum, average, month, and year support
  • safe delete and recovery semantics
  • SQL logging
  • multi-database JDBC template improvements
  • override-class and large-result handling

These were not just old-template maintenance tasks. They helped validate the capabilities that TeaQL later moved behind a cleaner library and runtime boundary.

The TeaQL Boundary

Modern TeaQL moved the generation boundary.

Instead of scattering generated source files across application workspaces, TeaQL focuses on generating versioned libraries, deterministic business APIs, query DSLs, object graph persistence behavior, and runtime capabilities that applications consume as artifacts.

That difference is important.

The old model was:

domain model -> generated source files inside the application workspace

The TeaQL model is:

domain model -> generated library/runtime artifact -> application dependency

The generated output becomes something that can be tested, published, versioned, upgraded, rolled back, and shared through normal release pipelines.

Why This Matters for AI Coding

AI-assisted coding changes the cost of generated source.

If thousands of generated files live beside handwritten business code, the AI agent has to navigate a noisy workspace. It may spend context on repeated scaffolding instead of the business workflow, integration, test, or product behavior that needs attention.

TeaQL's newer boundary gives AI agents a cleaner surface:

  • generated behavior lives behind stable APIs
  • business code depends on deterministic query and persistence contracts
  • repetitive infrastructure becomes library behavior
  • application changes stay focused on business intent

This does not remove generation. It makes generation more governable.

Why This Matters for DevOps

The same boundary helps DevOps.

Generated capabilities can move through normal engineering controls:

  • CI checks
  • automated tests
  • package publication
  • dependency upgrade
  • release notes
  • rollback
  • multi-environment deployment

That is much cleaner than repeatedly regenerating large source trees inside many application repositories.

How to Read the Old History

The older web-code-generator articles should be read as capability history.

They document how the system learned to generate persistence code, service code, frontend pages, mobile clients, forms, validation, search, aggregation, and runtime helpers. Some technologies mentioned there, such as JSP, DVA, older Android and Swift templates, or Taro miniapp generation, are no longer the current recommended stack.

Their lasting value is not the specific old framework. Their lasting value is the engineering lesson: complex business software has repeatable structures, and those structures should be generated from a domain model.

TeaQL is the AI-era productization of that lesson.

TeaQL is Born: Type-Safe Query Expressions

· One min read

This is where TeaQL began. In November 2022, the core runtime shipped with 95 files and 4,636 lines of code.

Core Capabilities

SQL Expression Parser

SQLExpressionParser, PropertyParser, and RawSqlParser form the foundation of type-safe query expressions:

Q.orders().filter(
Q.orders().customer().city().eq("Shanghai")
).executeForList(ctx);

Sub-Query Support

SubQueryParser (62 lines) enables nested queries expressed naturally in Java:

Q.orders().filter(
Q.orders().customer().city().eq("Shanghai")
).executeForList(ctx);

Dynamic Aggregation Framework

SimpleAggregation supports count, sum, avg, and more directly from the domain model.

Architecture at a Glance

  • Expression-based queries: Type-safe Java expressions compiled to SQL
  • Repository pattern: SQLRepository as the base with database-specific extensions
  • Sub-query composition: Nest queries naturally in Java
  • Aggregation framework: First-class aggregate function support

These patterns remain the foundation of TeaQL today.

Request DSL, Safe Delete, and Aggregation

· 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 2022 the generator's search layer became a real typed DSL.

The most important files were java_search_dsl_base.jsp, java_search_dsl_request.jsp, generated DAO helpers, SmartList, base entity templates, SQL logger templates, and scenario/data-manager builders. The code changes point to a clear feature: generated request objects became the main way to express reads, filters, selects, ordering, aggregation, and graph traversal.

BaseRequest as Generated Query Contract

The generated BaseRequest carried the common query model:

  • selected fields
  • parent selects
  • child selects
  • search criteria
  • order by clauses
  • pagination
  • aggregation
  • group by requests
  • dynamic attributes
  • cache hints

Object-specific request templates then generated typed methods such as filterBy..., select..., unselect..., and orderBy....

The request templates generated relationship-aware filters:

  • filter by parent request
  • filter by child request
  • refine ids through nested request results
  • select parent objects
  • select child lists
  • unselect nested relations

That made the query API model-driven. Developers could express graph-shaped reads without manually assembling joins or ad hoc SQL fragments.

Safe Delete and Update Semantics

The generated DSL also added safer write semantics:

  • safe delete
  • remove and recover naming
  • update operation support
  • version-aware id handling
  • list save improvements

This continued the same principle as generated reads: common business data operations should have deterministic generated APIs.

Aggregation, JSON Search, and Logging

The search layer also gained:

  • count fixes
  • sum and average helpers
  • month and year aggregation
  • JSON search expressions
  • list search from JSON
  • aggregation cache hints
  • SQL logging helpers

The result was a generated query surface that could serve UI screens, reports, dashboards, and API consumers without falling back to handwritten SQL for every case.

Spring Cloud Business Foundation

· 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 2021 the generator added a new backend target: Spring Cloud style business services.

The code diff shows new template areas under sky/springcloud and sky/WEB-INF/springcloud, plus repeated changes in entity service classes, base service classes, request templates, schema generation, feature graph code, and validation support. This was a move from monolithic generated managers toward service-oriented generated infrastructure.

Business Foundation Templates

The business-foundation templates covered generated service building blocks:

  • base service
  • entity service
  • remote service implementation
  • service adapter
  • base request
  • request DSL output
  • generated POJO imports and members

The generator was extracting common backend behavior into reusable service infrastructure.

Entity Service Layer

The entity service work added generated CRUD and lookup behavior:

  • id and type lookup
  • updater support
  • entity service methods
  • user context resolution
  • criteria parameter preparation
  • enum constants

This stage made generated services more explicit. Instead of all behavior living in manager templates, service templates could expose stable runtime contracts.

Schema and Validation

The Spring Cloud branch also brought table schema generation and validation improvements.

That pairing is important: generated services need generated persistence contracts, and generated persistence contracts need generated validation. Keeping those in the generator reduces mismatch between API, schema, and domain rules.

Why This Stage Mattered

The generator became less tied to a single Java web application shape.

Spring Cloud templates let the same model produce service infrastructure, request objects, remote service contracts, and schema metadata. This set up the later Request DSL and query API work by giving generated backend code a clearer service boundary.

Miniapp and IAM Generation

· 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 2020, mobile generation became a first-class backend and frontend concern.

The code changes touched Taro templates, mobile backend service templates, WeChat app service templates, IAM service templates, display mode helpers, tree services, generated view pages, and React view groups. The generator was learning to emit an app that could run across web admin and mobile-miniapp channels.

Taro and Mobile App Templates

The Taro templates introduced a generated mobile frontend target:

  • app entry generation
  • service center generation
  • mobile object pages
  • image and image-list support
  • generated app path handling

The templates also guarded generation based on model features, so mobile code was emitted only when the domain model declared the right capability.

Mobile Backend Service Layer

Mobile UI generation required backend support, so the Java templates added:

  • mobile app backend tasks
  • miniapp service view pages
  • list and detail service methods
  • display mode detection
  • generated view-home-page behavior

This kept the mobile app from becoming a separate manual project. The backend view layer and frontend app layer evolved together.

IAM Generation

The generator added IAM-related service templates:

  • base IAM service
  • WeChat mini-program handler
  • WeChat mobile handler
  • login method support
  • key-pair verification
  • avatar and phone update flows

Authentication and identity handling were becoming part of the generated application contract, not an afterthought bolted onto each project.

Tree and View Group Support

The generated UI also gained tree services and view group customization.

That made navigation and object presentation more model-aware. The generator could produce different views for different user entry points while keeping the same domain backend underneath.

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.

Metadata-Driven Search and Forms

· 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.

By 2018, the generator was no longer just expanding template count. It was strengthening the metadata model behind those templates.

The code changes repeatedly touched FieldDescriptor, ObjectDescriptor, ObjectCollection, PresentObjectDesc, query criteria templates, React object pages, dashboards, update forms, create forms, editable tables, and form processors. The recurring pattern was clear: generated code needed richer metadata to produce better behavior.

Descriptor Layer Becomes Central

FieldDescriptor and ObjectDescriptor became the shared language for:

  • Java field and column generation
  • React table columns
  • search form fields
  • parent and child relationship handling
  • presentation descriptors
  • permission and profile views

This reduced the number of one-off rules inside templates. Instead of each template rediscovering field meaning, descriptors carried more of that meaning.

Generated Search Criteria

The Java backend gained stronger query criteria generation:

  • base query criteria
  • DAO naming helpers
  • generated search methods
  • parent and child filtering
  • list and paging support

This prepared the path for the later Request DSL. Search was becoming a generated API, not just a controller convention.

Generated Form Runtime

The form-related code also became more systematic:

  • base form specs
  • form processors
  • object view detail templates
  • create and update form bodies
  • editable table templates

That made UI generation less page-by-page and more rule-driven. The generator could infer common form behavior from the domain model and presentation metadata.

Why This Stage Mattered

The main feature was not one specific screen. It was the move toward metadata-driven generation.

Once descriptors became expressive enough, the generator could produce backend APIs, frontend forms, search screens, and presentation views consistently. That is the foundation needed for typed query generation and deterministic business APIs later.

React DVA Admin Generation

· 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 2017 the generator started to produce a real web application layer.

The most active template areas shifted from only Java persistence to sky/react and supporting Java templates. New generated files covered DVA app entry points, object apps, models, services, tables, create forms, update forms, dashboards, routers, search views, and locale resources.

Generated Object Applications

The React templates turned each domain object into a predictable frontend module:

  • route registration
  • model state
  • service calls
  • table rendering
  • create and update forms
  • search screens
  • dashboard entry points

This was the frontend equivalent of generated DAO and manager code. Each object received the same operational surface, and project-specific customization could be layered around it.

SmartList and User Context

The Java side also evolved to support generated frontend needs:

  • SmartList helpers for list behavior
  • generated UserContext
  • base manager methods
  • common base entity behavior
  • operation tokens
  • locale resources

The frontend was not generated in isolation. It drove backend improvements so generated pages had the metadata, permissions, labels, and list behavior they needed.

Search and Presentation

Generated React search pages and generated Java presentation descriptors started to connect.

That created a full loop:

  1. model metadata describes fields and relationships
  2. backend code exposes list/search operations
  3. frontend code renders tables and forms
  4. locale and presentation metadata keep the generated UI usable

Why This Stage Mattered

This stage moved code generation from "generate the backend boilerplate" to "generate the operational app shell."

The generated application was still template-driven, but the feature surface became much broader: persistence, service methods, search screens, forms, tables, routing, and localization all came from the same model.

Generated Mobile Clients Join the Backend

· 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.

After the Java backend templates stabilized, the generator began producing client-side code.

The code diff shows new Android framework templates, Swift POJO templates, SwiftyJSON mapping helpers, remote manager templates, and sample HTTP request generation. This was not just adding another output language. It changed the generator from a backend scaffold into a multi-target system.

Android Application Skeleton

The Android templates introduced generated application structure:

  • activity framework files
  • entry page fragments
  • manifest templates
  • generated string resources
  • menu and naming convention helpers

The generated Android app did not need every screen to be handwritten. The same domain model could drive initial navigation, labels, and object entry pages.

Swift Object Generation

Swift support added another important boundary: generated domain objects could exist outside Java.

The Swift templates produced:

  • struct-based model objects
  • custom string conversion
  • JSON object mapping
  • reference-list handling
  • nil-safe member access

That forced the metadata layer to become less Java-specific. Field names, reference names, and type mapping had to work across languages.

Remote Manager Pattern

The remote manager templates connected mobile clients back to generated backend services.

Instead of treating the mobile app as a separate hand-coded consumer, the generator began producing both sides of the contract: server manager methods and client access code. That reduced drift between API shape and client expectations.

Why This Stage Mattered

The practical feature was mobile scaffolding. The architectural feature was bigger: one domain model could emit multiple runtime surfaces.

That idea later became essential for generating backend APIs, admin UI, mini-program code, search DSLs, and typed query APIs from the same model vocabulary.

From XML Models to Java Persistence Code

· 3 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.

The first useful shape of the generator was not a UI scaffold. It was a persistence scaffold.

Looking at the code changes, the core work concentrated around FieldDescriptor, ObjectDescriptor, CMRField, Java POJO templates, JDBC mapper templates, DAO implementations, SQL templates, and manager methods. That tells a clear story: the generator was learning to convert a domain description into an executable Java data layer.

Generated Java Object Model

The object templates started to produce more than plain fields:

  • typed Java members from model fields
  • constants for field names and table columns
  • generated toString, JSON serialization, and null-safe formatting
  • parent references and child reference lists
  • version fields for optimistic updates

The important shift was that the generated entity became a stable contract. DAO, mapper, manager, serializer, and JSP snippets could all depend on the same metadata.

DAO, Mapper, and Manager Layers

The generator added separate templates for:

  • DAO interface
  • JDBC template implementation
  • row mapper
  • load and save helpers
  • manager interface
  • manager implementation
  • manager exceptions

This separation mattered because it made generated code patchable by layer. SQL mapping problems could be fixed in mapper templates. Business entry points could evolve in manager templates. Persistence behavior could change without rewriting the domain model.

Multi-Database SQL Output

The early templates included MySQL, then added MSSQL support. Even at this stage, SQL was treated as generated infrastructure, not handwritten project code.

That design became a recurring theme: database differences should live behind generator templates and runtime helpers, while application code talks to generated typed APIs.

Relationship Handling

The generator also started handling parent and child relationships:

  • loading referenced objects
  • saving object graphs without duplicating unchanged rows
  • ordering fields deterministically
  • producing mapper code for references

This was the beginning of TeaQL's later object-graph save model. The early code was still template-heavy, but the direction was already visible: describe the domain once, then generate the repetitive persistence surface.