DEV Community

Cover image for TeaQL Java Runtime: Modular Refactor, Multi-Framework Ready, Rust-Aligned
Philip Z
Philip Z

Posted on • Originally published at teaql.io

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

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     │
└──────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Configuration comes from the entity model:

<task_execution_log
    _audit_mask_fields="detail"
    _audit_value_max_len="2048"
/>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Android

TeaQLDatabase db = new AndroidDatabase(sqLiteDatabase);
Enter fullscreen mode Exit fullscreen mode

Quarkus (CDI integration)

<dependency>
    <groupId>io.teaql</groupId>
    <artifactId>teaql-quarkus</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
@Inject
TeaQLDatabase db;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
@Inject
TeaQLDatabase db;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)