Skip to main content

41 posts tagged with "teaql"

View All Tags

Who Are Active? Human and Non-human Predicates in Generated Query APIs

· 2 min read
TeaQL Team
Core Team

Generated query APIs contain language, not only identifiers. That makes a small grammatical choice part of the public contract.

For a collection of people, TeaQL uses whoAreActive(), not whoIsActive(). For an ordinary human attribute it uses whoseEmailIs(...). A non-human entity uses whichAreActive() and withCodeIs(...).

These forms sound related, but they solve different problems: whose expresses possession, while who are agrees with the plural result set.

Pluralization Is Not `name + s`: A Code Generator Maintenance Rule

· 2 min read
TeaQL Team
Core Team

One of the smallest code generator shortcuts creates one of the most persistent API defects:

plural = name + "s"

It works for order, which makes it look harmless. Then it produces order_statuss, categorys, persons, childs, and inventorys.

During TeaQL's six-language acceptance work, we found this assumption in Python and Go query templates and in a Rust diagnostic. The generated code compiled often enough for the mistake to survive until an entity ending in status exposed it.

252 Dynamic Queries Later: TeaQL's Six-Language Order Search Acceptance

· 3 min read
TeaQL Team
Core Team

On August 12 we published a persistence baseline across six TeaQL runtimes and a nine-database Java matrix. That was useful evidence, but it was not the complete Order Search Feature.

The next acceptance run closed that distinction. Java, Rust, Go, Python, .NET and TypeScript each executed the generated Order Search API against PostgreSQL, MySQL and SQLite: 18 of 18 core cells passed, representing 252 successful positive dynamic scenario executions.

Six Languages, One Query Meaning: Closing TeaQL API Parity Gaps

· 2 min read
TeaQL Team
Core Team

“Supports six languages” should mean more than producing six directories that compile. The same model must preserve the same query meaning, loaded-state behavior and governance boundaries in every generated API.

TeaQL recently used Java's generated Request API as the semantic reference and ran paired generation tests across Rust, Go, Python, .NET and TypeScript. The work exposed gaps that ordinary runtime unit tests had missed.

One Execution Argument: Why TeaQL Queries Receive Only UserContext

· 2 min read
TeaQL Team
Core Team

A generated TeaQL query has exactly one caller-supplied runtime dependency argument: UserContext.

await request.execute_for_list(ctx)

There is no second data-service, provider or connection argument. Those dependencies—together with tenant identity, authenticated user, permissions and policy—are installed when the trusted context is initialized and resolved from it during execution.

.NET Nullable Is Not Load State: Preserving SQL NULL in E Expressions

· 4 min read
Philip Z
Architect

C# nullable reference types answer an important question: may this property have no value?

They do not answer a different data-access question: was this property loaded at all?

For a partially selected entity, string? Name == null can mean either SQL NULL or “the query never selected Name.” TeaQL's generated .NET E expressions model those as separate states and preserve the distinction through ADO.NET mapping.

Dynamic Search Is Not One WHERE Clause: TeaQL vs Conventional Rust

· 14 min read
Philip Z
Architect

A dynamic order page sounds ordinary: search, paginate, show status cards, and preview a few products. The interesting part begins when we ask what a production-grade implementation must actually contain—and count all of it.

We built the same read model with TeaQL Rust and conventional Rust plus SQLite. The first comparison was wrong in three important ways. Correcting those errors made the result more useful than the original benchmark.

Go E Expressions: Make Missing Preloads an Explicit Error

· 4 min read
Philip Z
Architect

Go programmers are comfortable with the comma-ok idiom:

value, ok := lookup(key)

That shape is useful for an E expression too, but one boolean cannot explain why a value is absent. In data-access code, there is a critical difference between a loaded null-like value and a field the query never selected.

TeaQL's generated Go E expressions preserve three states and provide two evaluation styles: TryEval() for explicit error handling and Eval() for strict fail-fast business logic.

Java E Expressions: Catch Unloaded Data Before Production

· 4 min read
Philip Z
Architect

Java applications have spent decades improving null handling, yet one dangerous ambiguity remains common in data-access code:

Does null mean the database value is NULL, or does it mean the query never loaded the property?

Those two states have completely different business meanings. Treating both as null can make a test pass and let the wrong decision reach production.

TeaQL's generated Java E expressions now preserve three states: Value, Null, and NotLoaded. A missing preload becomes a structured exception at the point of use, while a genuine SQL NULL remains a legitimate null value.

Python None Cannot Represent an Unloaded Field

· 4 min read
Philip Z
Architect

Python makes absence easy to express. A database NULL, a missing JSON key, an empty relation, and an optional function result can all become None.

That convenience becomes dangerous for partially loaded entities. If a query did not select task.name, returning None tells business code something the runtime does not know.

TeaQL's generated Python E expressions keep Value, Null, and NotLoaded separate. Loaded nulls evaluate to None; unselected fields raise a structured TeaQLNotLoadedError.

Rust Option Is Not Enough for Partially Loaded Entities

· 4 min read
Philip Z
Architect

Rust's Option<T> is one of the language's best tools. It forces absence into the type system and eliminates an entire class of null-pointer failures.

But an ORM or data runtime has another state that Option<T> cannot express on its own: the field was not loaded.

If both SQL NULL and an unselected column become None, business logic cannot tell a legitimate absence from an incomplete query. TeaQL Rust models the missing state explicitly with EvalResult::Value, EvalResult::Null, and EvalResult::NotLoaded.

From ORM Claims to Executed Evidence: Six TeaQL Runtimes and a Nine-Database Java Matrix

· 4 min read
TeaQL Team
Core Team

TeaQL now has a dated, executed database baseline across six generated language runtimes: Java, Rust, Go, Python, C#/.NET, and TypeScript.

The headline result is Java's real nine-database matrix:

tests=9 failures=0 errors=0 skipped=0

The databases were PostgreSQL, MySQL, SQLite, Oracle, DB2, DM8 (Dameng), SAP HANA, SQL Server, and DuckDB.

That is meaningful only because these were not adapter-discovery tests. The generated domain APIs compiled and executed schema creation, graph persistence, reconnect queries, optimistic updates, and direct database checks.

TypeScript undefined Is Not a Query Plan

· 4 min read
Philip Z
Architect

TypeScript can describe optional data precisely at compile time, but JavaScript still has several runtime forms of absence: undefined, null, a missing object key, and an empty collection.

For database-backed models, a missing key has another possible meaning: the query did not select the field.

TeaQL's generated TypeScript E expressions preserve Value, Null, and NotLoaded so that undefined does not silently stand in for an incomplete query plan.

Dynamic World Models: The Missing Requirements Layer for AI-Native Software Engineering

· 11 min read
Philip Z
Architect
Clariones Wang
Jacky Tian

AI can generate code. That does not mean it understands the world the code is supposed to represent.

This distinction is becoming one of the central problems in AI-native software engineering. A model may produce a plausible database schema, API, or workflow while missing an implicit business rule, confusing a role with a person, or treating a derived state as a source of truth. The output can be syntactically correct and still describe the wrong system.

The missing layer is not another code generator. It is a better representation of requirements.

We call this representation a Dynamic World Model: a reasoning framework for explaining how a domain works and why it is in its current state. In this framework, concepts participate in events, under constraints, to create and evolve relationships. Software is then generated or implemented as an executable projection of that explanation.

Numeric Entity IDs Across Rust, Java, Go, Python, SQL, and JSON

· 6 min read
TeaQL Team
Core Team

An entity ID looks like a small implementation detail until the same value has to survive a database join, a Rust service, a Java service, a Go worker, a Python script, and a browser. Each environment can represent a different set of integers, and accepting the widest type in one layer can make the whole system less portable.

TeaQL therefore separates three decisions:

  • persistence identities and foreign keys are compact numeric values;
  • their portable persisted range is the positive signed 64-bit range;
  • IDs crossing a JSON boundary are encoded as decimal strings.

The range is enormous for an identity space, but small enough to have a direct, unambiguous representation in the database and in the major server languages TeaQL supports.

Removing Reflection from TeaQL Java's Core Path: Runtime Determinism as Harness Engineering

· 12 min read
Philip Z
Architect

TeaQL Java did not remove every use of reflection from the repository. We did something more practical: we removed reflection from the core entity execution path, isolated the remaining reflective utilities, and added build-time guardrails to stop reflection from quietly returning.

GraalVM Native Image was an important forcing function, but it was not the highest-level reason for the work. The larger goal was to make the runtime environment more deterministic: important behavior should be explicit, inspectable, bounded, and enforceable before a production request reaches it.

We see that as harness engineering. The reliability of a system should not depend only on developers or coding agents remembering the right conventions. The surrounding environment—generated code, metadata, module boundaries, compiler-visible calls, build rules, and tests—should constrain execution into known-good paths.

TeaQL Adds Topcoat Support for Generated Rust Web Applications

· 5 min read
TeaQL Team
Core Team

TeaQL now supports Topcoat as a Rust web application target.

The new rust-web-topcoat target generates a server-rendered Topcoat application around the same TeaQL Rust domain crate used by our other application targets. It does not introduce a second shared core: a domain such as commerce continues to use commerce-core, while its framework-specific application crate is named commerce-web-topcoat.

The most important integration decision is the runtime boundary. Every HTTP request receives an independent TeaQL UserContext. TeaQL runtimes, repositories, executors, pools, and other TeaQL resources are accessed through that context and are never registered in Topcoat's application context.

Why Shorter Prompts Work Better: Building a Stronger TeaQL Agent Harness

· 7 min read
TeaQL Team
Core Team

Our most important practical finding today was simple:

The shorter and clearer the prompt, the more effectively the coding agent works.

That does not mean removing constraints. It means moving detailed constraints out of prose and into an executable harness: the TeaQL Generation Service, generated typed APIs, model-aware assist, compiler feedback, and tests.

Today we rebuilt TeaQL Agent Kit around that finding. The agent creates a typed domain contract, the Generation Service evaluates it, generated guidance constrains the implementation, and human review happens in parallel.

The smaller, clearer workflow is now distributed as a standard Agent Skill named build-teaql-app.

Building Java–Rust Microservices with TeaQL: Models, Events, and Audit Intent

· 14 min read
TeaQL Team
Core Team

The Architecture We Actually Want

Java remains a strong choice for workflow-heavy services such as merchant onboarding, KYC, approvals, accounting, and integration with an established enterprise platform. Rust is attractive for customer-facing services where resource use, predictable latency, and fast process startup matter.

Using both languages does not mean that two services should share one database or one persistence model. That would weaken their bounded contexts. The goal is more precise:

  • each service owns its model, database, and deployment lifecycle;
  • reviewed metadata generates the domain API for the language used by that service;
  • both stacks follow the same query-intent and mutation-audit conventions;
  • communication happens through an explicit, versioned integration contract.

We built a runnable Java–Rust payment reference project to test that design instead of treating it as an architecture diagram.

Turning Linux /proc into Domain Queries with TeaQL Rust

· 6 min read
TeaQL Team
Core Team

Reading system information on Linux is not difficult. Open /proc/meminfo, /proc/[pid]/stat, and /proc/[pid]/task, parse the text, and send the results to a terminal UI.

The harder part is keeping that code maintainable. As process fields grow, filtering becomes more complex, or the same system data needs to serve monitoring agents, alerting jobs, and management APIs, file reads and string parsing scattered through application code quickly become a liability.

Linux System Info using TeaQL demonstrates another approach: model Linux system information as domain objects, let TeaQL generate type-safe Rust query APIs, and use teaql-provider-linux to execute those queries against /proc. The example then builds an interactive process and thread monitor with ratatui.

Spring Boot 4.1.0 and Java 21 in TeaQL's Java Generation Targets

· 3 min read
TeaQL Team
Core Team

TeaQL's generated Java output has moved to a Java 21 baseline, and the Java generation targets now select Spring Boot 4.1.0.

The version change affects generated manifests and the language level available to generated code and application extensions. It does not remove the need to build and test each generated target against the matching TeaQL runtime.

The Return of E Expressions: Fluent Chaining and Structured Panics for AI Auto-Healing

· 4 min read
Philip Z
Architect

In the evolution of TeaQL, we've constantly navigated the tension between developer ergonomics and idiomatic Rust. We recently brought the E:: fluent expression chain back to Rust.

This is not a simple rollback. The new design uses reference chaining instead of cloning entity graphs and emits structured diagnostics when a relation was not loaded.

TeaQL 1.0.0 Release Notes

· 3 min read
TeaQL Team
Core Team

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)
.comment("Query candidates").purpose("Load data")
.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")
.comment("Query candidates").purpose("Load data")
.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)
.comment("Query candidates").purpose("Load data")
.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

· 3 min read
TeaQL Team
Core Team

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().comment("Query customers").purpose("Load data").selectName())
.selectLineItemList(Q.lineItems().selectSku().selectQuantity())
.countLineItems()
.comment("Query orders").purpose("Load data")
.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 Team
Core Team

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().comment("Query customers").purpose("Load data").selectName())
.selectLineItemList(Q.lineItems().selectSku().selectQuantity())
.countLineItems()
.orderByCreateTimeDescending()
.page(1, 20)
.comment("Query orders").purpose("Load data")
.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 vs MyBatis: an order page example

· 3 min read
TeaQL Team
Core Team

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()
.comment("Query users").purpose("Load data").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?

· 3 min read
TeaQL Team
Core Team

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()
.comment("Query users").purpose("Load data").filterWithId(userId)
.countOrder()
.statsFromOrder("statusWithCount", Q.orders().comment("Query orders").purpose("Load data").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 Team
Core Team

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().comment("Query customers").purpose("Load data").select_name())
.select_line_item_list_with(Q::line_items().comment("Query line_items").purpose("Load data").select_sku())
.which_statuses_are("PAID")
.page(1, 20)
.comment("Query orders").purpose("Load data")
.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.

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 Team
Core Team

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().comment("Query orders").purpose("Load data").customer().city().eq("Shanghai")
).comment("Query orders").purpose("Load data").executeForList(ctx);

Sub-Query Support

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

Q.orders().filter(
Q.orders().comment("Query orders").purpose("Load data").customer().city().eq("Shanghai")
).comment("Query orders").purpose("Load data").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.