TeaQL Java Runtime: Modular Refactor, Multi-Framework Ready, Rust-Aligned
We refactored the TeaQL Java runtime (teaql-java) to achieve three goals:
- JPMS module boundaries — seal internal packages, expose only what generated code needs
- Spring Boot independence — run without Spring Boot using a plain
main()function - Rust alignment — dual-layer audit logging, compile-time query enforcement,
RequestPolicy
Why Refactor?
The Java runtime had grown organically. All classes lived in flat packages — any code could access RepositoryAdaptor, GLobalResolver, GraphMutationEngine, and other internals. The Rust version had already solved this with proper module boundaries and ownership-based safety. We wanted the same discipline in Java.
Architecture: Three Layers
┌──────────────────────────────────────────────────┐
│ Layer 1: Application │
│ Spring Boot App / Plain Java App / Android App │
│ ↕ limited interfaces │
├──────────────────────────────────────────────────┤
│ Layer 2: Generated Code (teaql-code-gen output) │
│ Entity, Request(Q), Checker, BaseService │
│ ↕ limited interfaces │
├──────────────────────────────────────────────────┤
│ Layer 3: Runtime (teaql modules) │
│ public API + sealed internal implementation │
└──────────────────────────────────────────────── ──┘
JPMS Module Boundaries
We added module-info.java across the Java runtime modules. The core teaql module exports only what generated code needs:
module io.teaql {
// === Public API (generated code uses these) ===
exports io.teaql.data;
exports io.teaql.data.checker;
exports io.teaql.data.criteria;
exports io.teaql.data.meta;
exports io.teaql.data.translation;
exports io.teaql.data.web;
exports io.teaql.data.value;
exports io.teaql.data.lock;
exports io.teaql.data.log;
// === Sealed internals (precise authorization) ===
exports io.teaql.data.repository to io.teaql.sql, io.teaql.memory, io.teaql.sql.portable;
exports io.teaql.data.internal to io.teaql.autoconfigure, io.teaql.sql;
}
Internal packages stay outside the normal application API surface:
graph— mutation engineevent— event classesxls— Excel exportidgenerator— ID generationparser— expression parsersinternal—RepositoryAdaptor,GLobalResolver,TempRequest
Translator implementations under language are public runtime types, with
narrow reflective access where framework integration needs it.
We moved 5 classes from io.teaql.data to io.teaql.data.internal:
RepositoryAdaptor— graph save orchestratorGLobalResolver— static resolver holderTempRequest— temporary query wrapperRequestAggregationCacheKey— cache keySimpleChineseViewTranslator— internal translator
Spring Boot Independence
We created a TQLResolver interface that replaces Spring's ApplicationContext:
public interface TQLResolver {
<T> T getBean(Class<T> clazz);
<T> List<T> getBeans(Class<T> clazz);
<T> T getBean(String name);
Repository resolveRepository(String type);
EntityDescriptor resolveEntityDescriptor(String type);
}
For non-Spring environments, a simple Map-based implementation works:
static class SimpleResolver implements TQLResolver {
private final Map<Class<?>, Object> beans = new HashMap<>();
private final Map<String, Repository<?>> repos = new HashMap<>();
@Override public <T> T getBean(Class<T> clazz) {
return clazz.cast(beans.get(clazz));
}
@Override public Repository resolveRepository(String type) {
return repos.get(type);
}
}
A robot-kanban demo app runs with a plain main():
public static void main(String[] args) {
DataSource dataSource = createDataSource(); // SQLite
EntityMetaFactory metaFactory = new SimpleEntityMetaFactory();
// ... register metadata, repositories, checkers
TQLResolver resolver = new SimpleResolver(metaFactory, repos, checkers, config);
GLobalResolver.registerResolver(resolver);
UserContext ctx = new UserContext();
ctx.setRequestPolicy(new PurposeRequestPolicy());
ctx.setLogManager(new LogManager());
// Use Q/E API
Q.taskStatuses()
.comment("查询任务状态")
.purpose("展示看板")
.executeForList(ctx);
}
Compile-Time Query Enforcement
In Rust, you literally cannot call execute_for_list without setting comment — the type system prevents it. In Java, we achieve similar约束 with ExecutableRequest:
// purpose() is the terminal method — returns ExecutableRequest
Q.tasks()
.filterByName("xxx")
.comment("查询任务")
.purpose("展示看板") // → ExecutableRequest
.executeForList(ctx); // only ExecutableRequest has this
// Without purpose(): no ExecutableRequest, no executeForList
Q.tasks().executeForList(ctx); // compile error
The purpose() method validates that comment is set:
public ExecutableRequest<T> purpose(String purpose) {
if (comment == null || comment.isEmpty()) {
throw new RepositoryException(
"[PURPOSE FAILED] Missing .comment() on " + getTypeName());
}
this.purpose = purpose;
return new ExecutableRequest<>(this);
}
RequestPolicy (Rust Alignment)
We ported Rust's RequestPolicy trait to Java:
public interface RequestPolicy {
default void enforceSelect(UserContext ctx, SearchRequest<?> query) {}
default void enforceInsert(UserContext ctx, Entity entity) {}
default void enforceUpdate(UserContext ctx, Entity entity) {}
default void enforceDelete(UserContext ctx, Entity entity) {}
default void enforceRecover(UserContext ctx, Entity entity) {}
}
PurposeRequestPolicy enforces both query purpose and audit comments:
ctx.setRequestPolicy(new PurposeRequestPolicy());
// Rejects: no purpose
Q.taskStatuses().comment("x").executeForList(ctx);
// → [PURPOSE REQUIRED] Query on TaskStatus rejected.
// Rejects: no auditAs
entity.save(ctx);
// → [AUDIT REQUIRED] insert on Task rejected.
Dual-Layer Audit & Logging
The Rust design has two layers: a raw file log controlled by environment variables, and customizable app-level sinks with masked data. We ported this exactly.
Layer 1: Runtime (Environment Variables, No Code)
TEAQL_LOG_ENDPOINT=/var/log/teaql.log # file path
TEAQL_LOG_FORMAT=human # human | json
TEAQL_LOG_SELECT=true # log SELECT queries
TEAQL_LOG_MUTATION=true # log mutations
Writes raw, unmasked SQL and audit information. No code customization.
Layer 2: App Layer (Customizable, Masked)
LogManager logManager = new LogManager();
// Audit sink: receives masked events
logManager.addAuditSink((ctx, event) -> {
saveToDatabase(event);
sendToMessageQueue(event);
});
// Log sink: receives masked SQL logs
logManager.addLogSink(entry -> {
showOnUI(entry);
});
ctx.setLogManager(logManager);
Masking (Aligned with Rust)
The masking algorithm matches Rust's mask_audit_value and limit_audit_value:
// Field-level masking (from EntityDescriptor._audit_mask_fields)
maskAuditValue("12345678") → "12****78"
maskAuditValue("1234") → "****"
// Length truncation (from EntityDescriptor._audit_value_max_len)
limitAuditValue("very long string...", 200) → "head...tail"
Configuration comes from the entity model:
<task_execution_log
_audit_mask_fields="detail"
_audit_value_max_len="2048"
/>
Multi-Framework Support via TeaQLDatabase
We introduced TeaQLDatabase as the universal database abstraction layer. Portable runtimes share the same SQL repository shape (PortableSQLRepository), only the database driver differs:
Entity → PortableSQLRepository → TeaQLDatabase → Database
↑ ↑
Reuses SQL building Abstraction layer
(ExpressionHelper) Android: SQLiteDatabase
Quarkus: AgroalDataSource
Micronaut: DataSource
JVM test: JDBC
Android
TeaQLDatabase db = new AndroidDatabase(sqLiteDatabase);
Quarkus (CDI integration)
<dependency>
<groupId>io.teaql</groupId>
<artifactId>teaql-quarkus</artifactId>
</dependency>
@Inject
TeaQLDatabase db;
The TeaQLProducer provides default beans via CDI @Produces. Application can override any bean.
Micronaut (Bean Factory integration)
<dependency>
<groupId>io.teaql</groupId>
<artifactId>teaql-micronaut</artifactId>
</dependency>
@Inject
TeaQLDatabase db;
The TeaQLFactory provides default beans via @Factory + @Bean. Application can override any bean.
Plain Java (no framework)
TeaQLDatabase db = new QuarkusDatabase(dataSource); // or any JDBC DataSource
EntityMetaFactory metaFactory = new SimpleEntityMetaFactory();
// ... register metadata, repositories, checkers
TQLResolver resolver = new SimpleResolver(metaFactory, repos, checkers, config);
GLobalResolver.registerResolver(resolver);
Key change: ExpressionHelper.toSql() now accepts SQLColumnResolver instead of SQLRepository, so PortableSQLRepository can reuse the expression parsing logic without depending on spring-jdbc.
Summary Table
| Feature | Rust | Java |
|---|---|---|
| Module boundaries | Cargo workspace | JPMS module-info.java |
| Query enforcement | Compile-time (ownership) | ExecutableRequest + purpose() |
RequestPolicy | RequestPolicy trait | RequestPolicy interface |
| Audit masking | audit_mask_fields per entity | Same, via EntityDescriptor |
| SQL log | LogManager + env vars | Same |
| App sinks | SafeAuditEventSink | AuditEventSink + LogSink |
| Non-Boot runtime | Native | TQLResolver + SimpleResolver |
| Android-style portable SQL | N/A | teaql-sql-portable + TeaQLDatabase |
| Quarkus support | N/A | teaql-quarkus + CDI Producer |
| Micronaut support | N/A | teaql-micronaut + Bean Factory |
| DB abstraction | TeaQLDatabase trait | TeaQLDatabase interface |
The Java runtime is now modular, Spring Boot optional, multi-framework ready (Spring Boot / Quarkus / Micronaut / Android / Plain Java), and aligned with the Rust architecture on all critical design points.
