Add Java JNI round-trip test
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6111495..979e43c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -65,6 +65,17 @@
     steps:
       - uses: actions/checkout@v4
 
+      - name: Rust cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-cargo-
+
       - name: Set up JDK 8
         uses: actions/setup-java@v4
         with:
@@ -72,8 +83,13 @@
           java-version: '8'
           cache: maven
 
+      - name: Build JNI library
+        run: cargo build --release -p paimon-ftindex-jni
+
       - name: Test Java API
         run: mvn -q -f java/pom.xml test
+        env:
+          PAIMON_FTINDEX_JNI_LIB_PATH: ${{ github.workspace }}/target/release/libpaimon_ftindex_jni.so
 
   python:
     runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 6c0f2a8..c265aad 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
 - C FFI writer/reader/search JSON.
 - Java API and JNI bridge.
 - Python ctypes package.
-- Cross-boundary round-trip tests for Rust core, FFI, and Python.
+- Cross-boundary round-trip tests for Rust core, FFI, Java/JNI, and Python.
 
 Supported tokenizers in this first implementation:
 
@@ -44,7 +44,7 @@
 cargo test -p paimon-ftindex-core
 cargo test -p paimon-ftindex-ffi
 cargo build -p paimon-ftindex-ffi
-cargo check -p paimon-ftindex-jni
+cargo build -p paimon-ftindex-jni
 mvn -q -f java/pom.xml test
 PYTHONPATH=python python3 -m pytest -q python/tests
 ```
diff --git a/java/pom.xml b/java/pom.xml
index 7bf9eca..0354201 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -14,4 +14,13 @@
         <maven.compiler.target>8</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.13.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
 </project>
diff --git a/java/src/test/java/org/apache/paimon/index/fulltext/FullTextNativeRoundTripTest.java b/java/src/test/java/org/apache/paimon/index/fulltext/FullTextNativeRoundTripTest.java
new file mode 100644
index 0000000..c5c657a
--- /dev/null
+++ b/java/src/test/java/org/apache/paimon/index/fulltext/FullTextNativeRoundTripTest.java
@@ -0,0 +1,64 @@
+package org.apache.paimon.index.fulltext;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+public class FullTextNativeRoundTripTest {
+
+    @Test
+    public void testJavaNativeRoundTrip() throws Exception {
+        assumeTrue(
+                "PAIMON_FTINDEX_JNI_LIB_PATH must point to the built JNI library",
+                nativeLibraryConfigured());
+
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        try (FullTextIndexWriter writer = FullTextIndexWriter.create(Collections.emptyMap())) {
+            writer.addDocument(10L, "Apache Paimon full text search");
+            writer.addDocument(11L, "Tantivy only");
+            writer.writeIndex(output::write);
+        }
+
+        byte[] indexBytes = output.toByteArray();
+        FullTextIndexInput input =
+                (position, buffer, offset, length) ->
+                        readAt(indexBytes, position, buffer, offset, length);
+
+        try (FullTextIndexReader reader = new FullTextIndexReader(input)) {
+            FullTextSearchResult result = reader.search(FullTextQuery.match("paimon", "text"), 10);
+
+            assertEquals(1, result.size());
+            assertEquals(10L, result.rowIds()[0]);
+            assertTrue(result.scores()[0] > 0.0f);
+
+            try {
+                reader.search(FullTextQuery.match("paimon", "text"), 0);
+                fail("Expected non-positive search limit to fail");
+            } catch (IllegalArgumentException expected) {
+                assertEquals("search limit must be positive", expected.getMessage());
+            }
+        }
+    }
+
+    private static boolean nativeLibraryConfigured() {
+        String path = System.getenv("PAIMON_FTINDEX_JNI_LIB_PATH");
+        return path != null && !path.isEmpty() && new File(path).isFile();
+    }
+
+    private static void readAt(
+            byte[] source, long position, byte[] buffer, int offset, int length) throws IOException {
+        long end = position + length;
+        if (position < 0 || end > source.length || end < position) {
+            throw new IOException("read past end of index bytes");
+        }
+        System.arraycopy(source, (int) position, buffer, offset, length);
+    }
+}