MikArt

Native Access and Capabilities

Inspect backend support and drop to native clients without leaving Tava.

Tava keeps the portable API small and exposes backend-specific work explicitly. If a feature cannot be made portable without lying, use capabilities to detect it and native access to call the real client.

Capability Matrix

Capabilities.java
import eu.mikart.tava.capability.Feature;

var capabilities = tava.capabilities();

if (capabilities.supports(Feature.JSON)) {
    // Portable JSON work is available or emulated.
}

capabilities.matrix().forEach((feature, capability) ->
        System.out.printf("%s %s %s%n",
                feature,
                capability.level(),
                capability.detail()));

Support levels:

LevelMeaning
SUPPORTEDThe adapter supports the feature directly
EMULATEDThe adapter provides compatible behavior in the portable layer
NATIVE_ONLYUse native access for this feature
UNSUPPORTEDThe adapter does not provide the feature

Features include schema enforcement, unique constraints, secondary indexes, transactions, conditional writes, sorting, pagination, projection, joins, aggregation, TTL, full-text search, generated values, JSON, binary data, bulk reads, and bulk writes.

Native Handles

Use nativeHandle(...) when you need the underlying client object.

MongoNativeHandle.java
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Indexes;

MongoDatabase database = tava.nativeHandle(MongoDatabase.class);

database.getCollection("accounts")
        .createIndex(Indexes.text("bio"));

For JDBC adapters, use callback-style native access so the connection lifecycle stays under adapter control.

JdbcNativeAccess.java
import java.sql.Connection;

tava.nativeAccess().withNative(Connection.class, connection -> {
    try (var statement = connection.createStatement()) {
        statement.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm");
    }
    return null;
});

When To Use Native Access

Good fit

Full-text indexes, vendor extensions, bulk loaders, session settings, explain plans, transactions with backend-specific semantics, and maintenance operations.

Poor fit

Code that should behave the same across all adapters. Keep that in Schema, Entity<T>, Records, and Query.

Keep The Boundary Clear

RepositoryBoundary.java
final class AccountRepository {
    private final Tava tava;

    AccountRepository(Tava tava) {
        this.tava = tava;
    }

    Page<Account> activeAccounts() {
        return tava.entity(Account.class).find(Query.builder()
                .where(Predicate.eq("status", "active"))
                .sort(Sort.asc("email"))
                .limit(100)
                .build());
    }

    void createSearchIndex() {
        tava.nativeAccess().withNative(Connection.class, connection -> {
            try (var statement = connection.createStatement()) {
                statement.execute("CREATE INDEX IF NOT EXISTS accounts_email_trgm ON accounts USING gin (email gin_trgm_ops)");
            }
            return null;
        });
    }
}

This keeps ordinary application reads and writes portable while making the native dependency obvious at the call site.

Last updated on

Last updated on

On this page