This page describes what happens when the application calls authors.findByName("Sylvie Germain"): from the CDI proxy down to the pool, through JDQL → SQL translation and ResultSet → entity materialization. It is also the right place to understand why Mansart favors static codegen.

Overview — repository method call

Diagram

The whole hot path is reflection-free: QueryPlan instances are static constants prepared at <clinit>, parameter/attribute binding goes through MethodHandle cached once.

Codegen — APT (compile time)

mansart-data-processor is a javax.annotation.processing.Processor that scans compiled CUs:

  1. For each class annotated @Entity (Mansart or JPA) → generates Book.java (rich metamodel) and Book.java (JPA standard metamodel).

  2. For each interface annotated @Repository → generates BookRepositoryImpl.java (final public implementation class) + an entry in META-INF/mansart-repositories.list.

Source code generated under target/generated-sources/annotations/, marked @Generated("io.vidocq.mansart.data.processor"). The processor has no runtime dependency — it lives only in the consuming module’s APT classpath.

Diagram

Why APT and not runtime codegen?

  1. AOT-friendly — GraalVM native-image, Leyden CDS: no proxy to register, everything is in the bytecode at build time.

  2. Diagnosable — generated code is readable, debuggable, visible in stack traces.

  3. Zero warmup — no dynamic classloading on first call.

  4. JPMS-strict-friendly — generated classes are in the same package as sources, no need for opens.

Codegen — Class-File API (build time)

mansart-pool generates PooledConnectionProxy at build via JEP 484 Class-File API. The proxy implements java.sql.Connection, delegates every method to the physical connection, and intercepts close() to return the connection to the pool instead of closing it.

Why Class-File API rather than java.lang.reflect.Proxy:

  • Connection has 50+ methods — a reflective proxy costs an invoke per call.

  • The Class-File API produces standard bytecode, JIT-friendly from the first call.

  • No ASM / Byte Buddy dependency — matches the ecosystem’s "zero external dependency" rule.

Pool acquire / release

Diagram
  • Semaphore permits — hard pool bound, fair=false (perf).

  • ConcurrentLinkedDeque idle — lock-free stack, push/pop at the head (hot LIFO).

  • ConcurrentHashMap.newKeySet inUse — set of borrowed connections, for metrics and leak detection.

  • Housekeeper virtual thread — periodic wake-up, evicts connections that are too old or idle, pre-warms up to minSize.

Details and numbers: see lien:https://codeberg.org/Vidocq/mansart/src/branch/main/mansart-pool/BENCH.md[mansart-pool BENCH.md].

Threading — virtual threads

  • All blocking JDBC executions (PreparedStatement.execute*, ResultSet.next) run on virtual threads. No platform pool.

  • The JDBC connection is never held across a synchronized block that wraps a blocking operation (anti-pattern: thread pinning).

  • The transactional connection is carried by ScopedValue<Connection> CURRENT — automatic propagation into StructuredTaskScope.fork(…​) with no application code.

  • mansart-pool validates in CI with -Djdk.tracePinnedThreads=full that no pinning appears under 1000 parallel borrows.

ResultSet → entity mapping

No reflection. The _Book metamodel captures, in <clinit>, one MethodHandle setter per attribute, obtained via MethodHandles.privateLookupIn(Book.class, lookup). For each row:

Book row = constructor.invoke();           // MethodHandle constructor
_Book.title.setter.invokeExact(row, rs.getString(1));
_Book.author.setter.invokeExact(row, refLoader.get(rs.getLong(2)));
// ...

invokeExact on a MethodHandle is inlined by C2 — perf equivalent to direct field access after warmup.

Transactions — propagation and XA

  • Single-resource (the dominant case) — the @Transactional interceptor opens/closes the transaction. The JDBC connection is enlisted as a degenerate XAResource (onePhase=true).

  • Multi-resource (M4) — true 2PC: prepare then commit on each enlisted XAResource. Recovery log (M5) records PREPARED transactions that did not complete, for recovery on reboot.

  • Suspend / resumeTransactionManager.suspend() removes the context from the current ScopedValue and returns it; resume(t) reinstates it. Typical batch case: a worker job suspending the caller’s TX to issue a clean INSERT/COMMIT.

See lien:https://codeberg.org/Vidocq/mansart/src/branch/main/mansart-transactions/PLAN.md[mansart-transactions PLAN.md] for the M1-M8 roadmap.