Skip to main content

TeaQL Java Runtime: Modular Refactor, Multi-Framework Ready, Rust-Aligned

· 6 min read
Philip Z
Architect

We refactored the TeaQL Java runtime (teaql-java) to achieve three goals:

  1. JPMS module boundaries — seal internal packages, expose only what generated code needs
  2. Spring Boot independence — run without Spring Boot using a plain main() function
  3. 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 engine
  • event — event classes
  • xls — Excel export
  • idgenerator — ID generation
  • parser — expression parsers
  • internalRepositoryAdaptor, 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 orchestrator
  • GLobalResolver — static resolver holder
  • TempRequest — temporary query wrapper
  • RequestAggregationCacheKey — cache key
  • SimpleChineseViewTranslator — 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

FeatureRustJava
Module boundariesCargo workspaceJPMS module-info.java
Query enforcementCompile-time (ownership)ExecutableRequest + purpose()
RequestPolicyRequestPolicy traitRequestPolicy interface
Audit maskingaudit_mask_fields per entitySame, via EntityDescriptor
SQL logLogManager + env varsSame
App sinksSafeAuditEventSinkAuditEventSink + LogSink
Non-Boot runtimeNativeTQLResolver + SimpleResolver
Android-style portable SQLN/Ateaql-sql-portable + TeaQLDatabase
Quarkus supportN/Ateaql-quarkus + CDI Producer
Micronaut supportN/Ateaql-micronaut + Bean Factory
DB abstractionTeaQLDatabase traitTeaQLDatabase 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.