This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new db25b7cd0f [docs] Restructure Program API guides and add SVG diagrams 
(#9730)
db25b7cd0f is described below

commit db25b7cd0fa14789ae9a281a5717352828d3c423
Author: Jingsong Lee <[email protected]>
AuthorDate: Fri Sep 11 13:01:24 2026 +0800

    [docs] Restructure Program API guides and add SVG diagrams (#9730)
---
 docs/docs/program-api/catalog-api.md        | 388 +++++++++----------------
 docs/docs/program-api/cpp-api.md            | 102 ++++---
 docs/docs/program-api/file-cache.mdx        |  86 ++++--
 docs/docs/program-api/flink-api.mdx         | 175 +++++++-----
 docs/docs/program-api/index.md              |  43 +++
 docs/docs/program-api/java-api.mdx          | 426 +++++-----------------------
 docs/docs/program-api/java-reading.md       | 205 +++++++++++++
 docs/docs/program-api/java-types.md         |  96 +++++++
 docs/docs/program-api/java-writing.md       | 182 ++++++++++++
 docs/docs/program-api/rest-api.mdx          |  37 ++-
 docs/docs/program-api/rust-api.md           |  25 +-
 docs/sidebars.js                            |  25 +-
 docs/static/img/program-api-local-cache.svg |  54 ++++
 docs/static/img/program-api-overview.svg    |  63 ++++
 docs/static/img/program-api-read-flow.svg   |  52 ++++
 docs/static/img/program-api-write-flow.svg  |  56 ++++
 16 files changed, 1252 insertions(+), 763 deletions(-)

diff --git a/docs/docs/program-api/catalog-api.md 
b/docs/docs/program-api/catalog-api.md
index 032f5ad9bd..3afb609413 100644
--- a/docs/docs/program-api/catalog-api.md
+++ b/docs/docs/program-api/catalog-api.md
@@ -1,6 +1,6 @@
 ---
 title: "Catalog API"
-sidebar_position: 4
+sidebar_position: 2
 ---
 
 <!--
@@ -24,314 +24,208 @@ under the License.
 
 # Catalog API
 
-## Create Database
+Use `Catalog` to manage databases and tables, and to load a `Table` for 
reading or writing.
+Catalog configuration and table options have different scopes: configure the 
warehouse and metastore
+on the catalog; put storage and read/write behavior in the table schema's 
options.
 
-You can use the catalog to create databases. The created databases are 
persistence in the file system.
+## Set up the examples
+
+Add the [Java dependency](java-api#dependency) and save the
+[`CreateCatalog` helper](java-api#create-catalog). The snippets below are 
method-body fragments.
+Place the operations you want to run inside this catalog scope:
 
 ```java
 import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.catalog.PropertyChange;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.schema.SchemaChange;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
 
-public class CreateDatabase {
+import java.util.Arrays;
+import java.util.List;
+
+public class CatalogExample {
 
-    public static void main(String[] args) {
-        try {
-            Catalog catalog = CreateCatalog.createFilesystemCatalog();
-            catalog.createDatabase("my_db", false);
-        } catch (Catalog.DatabaseAlreadyExistException e) {
-            // do something
+    public static void main(String[] args) throws Exception {
+        try (Catalog catalog = CreateCatalog.createFilesystemCatalog()) {
+            // Insert the relevant snippets here.
         }
     }
 }
 ```
 
-## Determine Whether Database Exists
+The `ignoreIfExists` and `ignoreIfNotExists` flags handle an existing or 
missing object, respectively.
+They do not suppress other validation errors. Use `false` when an unexpected 
catalog state should
+fail the operation. The examples propagate exceptions; applications can handle 
the corresponding
+`Catalog.*Exception` at their error-handling boundary.
 
-You can use the catalog to determine whether the database exists
+## Manage databases
+
+### Create Database
 
 ```java
-import org.apache.paimon.catalog.Catalog;
+catalog.createDatabase("my_db", false);
+```
 
-public class DatabaseExists {
+### Determine Whether Database Exists
 
-    public static void main(String[] args) {
-        Catalog catalog = CreateCatalog.createFilesystemCatalog();
-        boolean exists = catalog.databaseExists("my_db");
-    }
-}
+```java
+boolean exists = catalog.databaseExists("my_db");
 ```
 
-## List Databases
-
-You can use the catalog to list databases.
+### List Databases
 
 ```java
-import org.apache.paimon.catalog.Catalog;
+List<String> databases = catalog.listDatabases();
+```
 
-import java.util.List;
+### Alter Database
 
-public class ListDatabases {
+Use `PropertyChange` for database properties. Hive, JDBC, and REST catalogs 
support this operation;
+the filesystem catalog does not. Run this fragment with a suitable catalog, 
such as one created
+with `CreateCatalog.createHiveCatalog()`, and an existing database.
 
-    public static void main(String[] args) {
-        Catalog catalog = CreateCatalog.createFilesystemCatalog();
-        List<String> databases = catalog.listDatabases();
-    }
-}
+```java
+List<PropertyChange> changes = Arrays.asList(
+        PropertyChange.setProperty("owner", "analytics"),
+        PropertyChange.removeProperty("obsolete-property"));
+catalog.alterDatabase("my_db", changes, false);
 ```
 
-## Drop Database
+## Manage tables
+
+### Create Table
 
-You can use the catalog to drop database.
+This is the same schema used by the Java reading and writing examples. Create 
`my_db` first.
+For partitioned tables with fixed buckets, include partition columns in the 
primary key.
+Tables that update a key across partitions require a different layout; see
+[cross-partition 
upserts](../primary-key-table/data-distribution#cross-partitions-upsert).
 
 ```java
-import org.apache.paimon.catalog.Catalog;
+Identifier identifier = Identifier.create("my_db", "my_table");
+Schema schema = Schema.newBuilder()
+        .column("f0", DataTypes.STRING().notNull())
+        .column("f1", DataTypes.INT())
+        .primaryKey("f0")
+        .option("bucket", "2")
+        .build();
+catalog.createTable(identifier, schema, false);
+```
 
-public class DropDatabase {
+### Get Table
 
-    public static void main(String[] args) {
-        try {
-            Catalog catalog = CreateCatalog.createFilesystemCatalog();
-            catalog.dropDatabase("my_db", false, true);
-        } catch (Catalog.DatabaseNotEmptyException e) {
-            // do something
-        } catch (Catalog.DatabaseNotExistException e) {
-            // do something
-        }
-    }
-}
+```java
+Table table = catalog.getTable(Identifier.create("my_db", "my_table"));
 ```
 
-## Alter Database
+Use the returned table with [Java Reads](java-reading) or [Java 
Writes](java-writing).
 
-You can use the catalog to alter database's properties.(ps: only support hive 
and jdbc catalog)
+### Determine Whether Table Exists
 
 ```java
-import java.util.ArrayList;
-import org.apache.paimon.catalog.Catalog;
-
-public class AlterDatabase {
-
-    public static void main(String[] args) {
-        try {
-            Catalog catalog = CreateCatalog.createHiveCatalog();
-            List<DatabaseChange> changes = new ArrayList<>();
-            changes.add(DatabaseChange.setProperty("k1", "v1"));
-            changes.add(DatabaseChange.removeProperty("k2"));
-            catalog.alterDatabase("my_db", changes, true);
-        } catch (Catalog.DatabaseNotExistException e) {
-            // do something
-        }
-    }
-}
+boolean exists = catalog.tableExists(Identifier.create("my_db", "my_table"));
 ```
 
-## Determine Whether Table Exists
-
-You can use the catalog to determine whether the table exists
+### List Tables
 
 ```java
-import org.apache.paimon.catalog.Catalog;
-import org.apache.paimon.catalog.Identifier;
+List<String> tables = catalog.listTables("my_db");
+```
 
-public class TableExists {
+### Rename Table
 
-    public static void main(String[] args) {
-        Identifier identifier = Identifier.create("my_db", "my_table");
-        Catalog catalog = CreateCatalog.createFilesystemCatalog();
-        boolean exists = catalog.tableExists(identifier);
-    }
-}
+```java
+catalog.renameTable(
+        Identifier.create("my_db", "my_table"),
+        Identifier.create("my_db", "renamed_table"),
+        false);
 ```
 
-## List Tables
+Subsequent operations must use the new identifier.
 
-You can use the catalog to list tables.
-
-```java
-import org.apache.paimon.catalog.Catalog;
+## Alter Table
 
-import java.util.List;
+Pass one `SchemaChange` or an ordered list of changes to `catalog.alterTable`. 
The following example
+uses a separate table so that schema changes do not invalidate the Java 
read/write walkthrough.
 
-public class ListTables {
+### Create a table for schema changes
 
-    public static void main(String[] args) {
-        try {
-            Catalog catalog = CreateCatalog.createFilesystemCatalog();
-            List<String> tables = catalog.listTables("my_db");
-        } catch (Catalog.DatabaseNotExistException e) {
-            // do something
-        }
-    }
-}
+```java
+Identifier alterIdentifier = Identifier.create("my_db", "schema_example");
+Schema alterSchema = Schema.newBuilder()
+        .column("id", DataTypes.STRING().notNull())
+        .column("region", DataTypes.STRING().notNull())
+        .column("amount", DataTypes.INT().notNull())
+        .column("description", DataTypes.STRING())
+        .column("obsolete", DataTypes.STRING())
+        .column("details", DataTypes.ROW(
+                new DataField(0, "city", DataTypes.STRING().notNull())))
+        .primaryKey("id", "region")
+        .partitionKeys("region")
+        .option("bucket", "2")
+        .option("snapshot.num-retained.max", "20")
+        .build();
+catalog.createTable(alterIdentifier, alterSchema, false);
 ```
 
-## Drop Table
-
-You can use the catalog to drop table.
+### Apply schema and option changes
 
 ```java
-import org.apache.paimon.catalog.Catalog;
-import org.apache.paimon.catalog.Identifier;
+List<SchemaChange> changes = Arrays.asList(
+        SchemaChange.setOption("snapshot.time-retained", "2h"),
+        SchemaChange.removeOption("snapshot.num-retained.max"),
+        SchemaChange.addColumn("note", DataTypes.STRING(), "Optional note",
+                SchemaChange.Move.after("note", "description")),
+        SchemaChange.renameColumn("description", "description_text"),
+        SchemaChange.dropColumn("obsolete"),
+        SchemaChange.updateColumnComment(new String[] {"amount"}, "Order 
amount"),
+        SchemaChange.updateColumnComment(new String[] {"details", "city"}, 
"City name"),
+        SchemaChange.updateColumnType("amount", DataTypes.BIGINT()),
+        SchemaChange.updateColumnPosition(SchemaChange.Move.first("amount")),
+        SchemaChange.updateColumnNullability(new String[] {"amount"}, true),
+        SchemaChange.updateColumnNullability(new String[] {"details", "city"}, 
true));
+catalog.alterTable(alterIdentifier, changes, false);
+
+// Load the updated schema before building new readers or writers.
+Table updatedTable = catalog.getTable(alterIdentifier);
+```
 
-public class DropTable {
+### Schema constraints
 
-    public static void main(String[] args) {
-        Identifier identifier = Identifier.create("my_db", "my_table");
-        try {
-            Catalog catalog = CreateCatalog.createFilesystemCatalog();
-            catalog.dropTable(identifier, false);
-        } catch (Catalog.TableNotExistException e) {
-            // do something
-        }
-    }
-}
-```
+- Newly added columns must be nullable.
+- Primary-key and partition-column types cannot be changed. Primary-key 
nullability cannot be changed.
+- The example widens `INT` to `BIGINT` and relaxes `NOT NULL`. Tightening a 
nullable column to
+  `NOT NULL` is disabled by default through 
`alter-column-null-to-not-null.disabled`.
+- Nested field operations use a field path, for example `new String[] 
{"details", "city"}`.
+  Supported type changes depend on the source and target types; replacing an 
entire row type is
+  different from changing one nested field.
 
-## Rename Table
+See [schema evolution](../flink/sql-alter) for supported changes and their 
constraints.
 
-You can use the catalog to rename a table.
+## Remove objects
 
-```java
-import org.apache.paimon.catalog.Catalog;
-import org.apache.paimon.catalog.Identifier;
+Drop operations remove catalog objects and can remove their stored data. Run 
these separately from
+the read/write walkthrough, using the identifier that currently exists.
 
-public class RenameTable {
-
-    public static void main(String[] args) {
-        Identifier fromTableIdentifier = Identifier.create("my_db", 
"my_table");
-        Identifier toTableIdentifier = Identifier.create("my_db", 
"test_table");
-        try {
-            Catalog catalog = CreateCatalog.createFilesystemCatalog();
-            catalog.renameTable(fromTableIdentifier, toTableIdentifier, false);
-        } catch (Catalog.TableAlreadyExistException e) {
-            // do something
-        } catch (Catalog.TableNotExistException e) {
-            // do something
-        }
-    }
-}
-```
+### Drop Table
 
-## Alter Table
+```java
+catalog.dropTable(Identifier.create("my_db", "renamed_table"), false);
+```
 
-You can use the catalog to alter a table, but you need to pay attention to the 
following points.
+### Drop Database
 
-- Column %s cannot specify NOT NULL in the %s table.
-- Cannot update partition column type in the table.
-- Cannot change nullability of primary key.
-- If the type of the column is nested row type, update the column type is not 
supported.
-- Update column to nested row type is not supported.
+Use `cascade=false` to reject dropping a nonempty database:
 
 ```java
-import org.apache.paimon.catalog.Catalog;
-import org.apache.paimon.catalog.Identifier;
-import org.apache.paimon.schema.Schema;
-import org.apache.paimon.schema.SchemaChange;
-import org.apache.paimon.types.DataField;
-import org.apache.paimon.types.DataTypes;
-
-import com.google.common.collect.Lists;
+catalog.dropDatabase("my_db", false, false);
+```
 
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-
-public class AlterTable {
-
-    public static void main(String[] args) {
-        Identifier identifier = Identifier.create("my_db", "my_table");
-
-        Map<String, String> options = new HashMap<>();
-        options.put("bucket", "4");
-
-        Catalog catalog = CreateCatalog.createFilesystemCatalog();
-        catalog.createDatabase("my_db", false);
-
-        try {
-            catalog.createTable(
-                    identifier,
-                    new Schema(
-                            Lists.newArrayList(
-                                    new DataField(0, "col1", 
DataTypes.STRING(), "field1"),
-                                    new DataField(1, "col2", 
DataTypes.STRING(), "field2"),
-                                    new DataField(2, "col3", 
DataTypes.STRING(), "field3"),
-                                    new DataField(3, "col4", 
DataTypes.BIGINT(), "field4"),
-                                    new DataField(
-                                            4,
-                                            "col5",
-                                            DataTypes.ROW(
-                                                    new DataField(
-                                                            5, "f1", 
DataTypes.STRING(), "f1"),
-                                                    new DataField(
-                                                            6, "f2", 
DataTypes.STRING(), "f2"),
-                                                    new DataField(
-                                                            7, "f3", 
DataTypes.STRING(), "f3")),
-                                            "field5"),
-                                    new DataField(8, "col6", 
DataTypes.STRING(), "field6")),
-                            Lists.newArrayList("col1"), // partition keys
-                            Lists.newArrayList("col1", "col2"), // primary key
-                            options,
-                            "table comment"),
-                    false);
-        } catch (Catalog.TableAlreadyExistException e) {
-            // do something
-        } catch (Catalog.DatabaseNotExistException e) {
-            // do something
-        }
+Use `cascade=true` only when you intend to remove the database and its tables:
 
-        // add option
-        SchemaChange addOption = 
SchemaChange.setOption("snapshot.time-retained", "2h");
-        // add column
-        SchemaChange addColumn = SchemaChange.addColumn("col1_after", 
DataTypes.STRING());
-        // add a column after col1
-        SchemaChange.Move after = SchemaChange.Move.after("col1_after", 
"col1");
-        SchemaChange addColumnAfterField =
-                SchemaChange.addColumn("col7", DataTypes.STRING(), "", after);
-        // rename column
-        SchemaChange renameColumn = SchemaChange.renameColumn("col3", 
"col3_new_name");
-        // drop column
-        SchemaChange dropColumn = SchemaChange.dropColumn("col6");
-        // update column comment
-        SchemaChange updateColumnComment =
-                SchemaChange.updateColumnComment(new String[]{"col4"}, "col4 
field");
-        // update nested column comment
-        SchemaChange updateNestedColumnComment =
-                SchemaChange.updateColumnComment(new String[]{"col5", "f1"}, 
"col5 f1 field");
-        // update column type
-        SchemaChange updateColumnType = SchemaChange.updateColumnType("col4", 
DataTypes.DOUBLE());
-        // update column position, you need to pass in a parameter of type Move
-        SchemaChange updateColumnPosition =
-                
SchemaChange.updateColumnPosition(SchemaChange.Move.first("col4"));
-        // update column nullability
-        SchemaChange updateColumnNullability =
-                SchemaChange.updateColumnNullability(new String[]{"col4"}, 
false);
-        // update nested column nullability
-        SchemaChange updateNestedColumnNullability =
-                SchemaChange.updateColumnNullability(new String[]{"col5", 
"f2"}, false);
-
-        SchemaChange[] schemaChanges =
-                new SchemaChange[]{
-                        addOption,
-                        removeOption,
-                        addColumn,
-                        addColumnAfterField,
-                        renameColumn,
-                        dropColumn,
-                        updateColumnComment,
-                        updateNestedColumnComment,
-                        updateColumnType,
-                        updateColumnPosition,
-                        updateColumnNullability,
-                        updateNestedColumnNullability
-                };
-        try {
-            catalog.alterTable(identifier, Arrays.asList(schemaChanges), 
false);
-        } catch (Catalog.TableNotExistException e) {
-            // do something
-        } catch (Catalog.ColumnAlreadyExistException e) {
-            // do something
-        } catch (Catalog.ColumnNotExistException e) {
-            // do something
-        }
-    }
-}
+```java
+catalog.dropDatabase("my_db", false, true);
 ```
diff --git a/docs/docs/program-api/cpp-api.md b/docs/docs/program-api/cpp-api.md
index f9962ccf9e..5d208422e1 100644
--- a/docs/docs/program-api/cpp-api.md
+++ b/docs/docs/program-api/cpp-api.md
@@ -1,9 +1,8 @@
 ---
-title: "Cpp API"
-sidebar_position: 6
+title: "C++ API"
+sidebar_position: 8
 ---
 
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -23,15 +22,24 @@ specific language governing permissions and limitations
 under the License.
 -->
 
-# Cpp API
+# C++ API
+
+[Paimon C++](https://github.com/apache/paimon-cpp) provides native table 
access for C++ applications
+and engines. It exchanges columnar data through the Arrow C Data Interface.
+
+This walkthrough follows the same workflow as the Java API: create a catalog 
and table, prepare and
+commit a batch, then plan splits and read them. The snippets are function-body 
fragments returning
+`paimon::Status` (place includes at file scope); `PAIMON_RETURN_NOT_OK` and 
`PAIMON_ASSIGN_OR_RAISE` propagate failures to its caller.
+The `PrepareData` helper returns `arrow::Result` and uses Arrow's error macros 
instead.
 
-[Paimon C++](https://github.com/apache/paimon-cpp.git) is a high-performance 
C++ implementation of Apache Paimon. 
-Paimon C++ aims to provide a native, high-performance and extensible 
implementation 
-that allows native engines to access the Paimon datalake format with maximum 
efficiency.
+For complete headers and a runnable application, start with the
+[C++ examples](https://paimon.apache.org/docs/cpp/examples/index.html). The 
C++ project has its own
+release cycle; use its build instructions and API reference for the version 
you select.
 
 ## Environment Settings
 
-You can checkout the [document](https://paimon.apache.org/docs/cpp/index.html) 
for more details about environment settings.
+Follow the [C++ build guide](https://paimon.apache.org/docs/cpp/building.html) 
to install prerequisites
+and select optional filesystem, file-format, and catalog components. A basic 
source build is:
 
 ```sh
 git clone https://github.com/apache/paimon-cpp.git
@@ -45,36 +53,37 @@ make install
 
 ## Create Catalog
 
-Before coming into contact with the Table, you need to create a Catalog.
+Create a filesystem catalog for a warehouse. Reuse these options and 
identifiers in the following
+fragments; choose a fresh table name when running the walkthrough again.
 
 ```c++
 #include "paimon/catalog/catalog.h"
 
-// Note that keys and values are all string
+const std::string root_path = "/tmp/paimon-cpp-warehouse";
+const std::string db_name = "my_db";
+const std::string table_name = "my_table";
 std::map<std::string, std::string> options;
 PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::Catalog> catalog,
                        paimon::Catalog::Create(root_path, options));
 ```
 
-Current C++ Paimon only supports filesystem catalog. In the future, we will 
support REST catalog.
-See [Catalog](../concepts/catalog).
-
-You can use the catalog to create table for writing data.
+C++ also supports a REST catalog when built with `PAIMON_ENABLE_REST=ON`.
+See the [C++ catalog 
guide](https://paimon.apache.org/docs/cpp/user_guide/catalog.html) for options
+and supported operations. For REST-managed tables, use the table location 
returned by the catalog;
+the path construction below is specific to the filesystem catalog.
 
 ## Create Database
 
-Table is located in a database. If you want to create table in a new database, 
you should create it.
+Create the database before the table:
 
 ```c++
-PAIMON_RETURN_NOT_OK(catalog->CreateDatabase('database_name', options, 
/*ignore_if_exists=*/false));
+PAIMON_RETURN_NOT_OK(catalog->CreateDatabase(db_name, options, 
/*ignore_if_exists=*/false));
 ```
 
 ## Create Table
 
-Table schema contains fields definition, partition keys, primary keys, table 
options.
-The field definition is described by `Arrow::Schema`. All arguments except 
fields definition are optional.
-
-for example:
+Define fields using an `arrow::Schema`, then export it through the Arrow C 
Data Interface.
+This example creates an unpartitioned append table without primary keys.
 
 ```c++
 arrow::FieldVector fields = {
@@ -101,11 +110,13 @@ See [Data 
Types](https://paimon.apache.org/docs/cpp/user_guide/data_types.html)
 
 ## Batch Write
 
-Paimon table write is Two-Phase Commit, you can write many times, but once 
committed, no more data can be written.
-C++ Paimon uses Apache Arrow as [in-memory format], check out 
[document](https://paimon.apache.org/docs/cpp/user_guide/arrow.html)
-for more details.
+First construct an Arrow batch, then write it and prepare commit messages. The 
committer publishes
+the prepared changes. In a distributed application, collect messages from the 
participating writers
+before committing. See the [memory format 
guide](https://paimon.apache.org/docs/cpp/user_guide/arrow.html)
+for ownership and Arrow conversion details.
+
+### Build a batch
 
-for example:
 ```c++
 arrow::Result<std::shared_ptr<arrow::StructArray>> PrepareData(const 
arrow::FieldVector& fields) {
     arrow::StringBuilder f0_builder;
@@ -135,6 +146,8 @@ arrow::Result<std::shared_ptr<arrow::StructArray>> 
PrepareData(const arrow::Fiel
 }
 ```
 
+### Write and commit
+
 ```c++
 std::string table_path = root_path + "/" + db_name + ".db/" + table_name;
 std::string commit_user = "some_commit_user";
@@ -172,39 +185,39 @@ PAIMON_RETURN_NOT_OK(committer->Commit(commit_message));
 
 ## Batch Read
 
-### Predicate pushdown
-
-A `ReadContextBuilder` is used to pass context to reader, push down and filter 
is done by reader.
+Configure the reader, plan the splits, then consume each batch. When 
distributing reads, plan once
+and assign splits to reader tasks.
 
-```c++
-ReadContextBuilder read_context_builder(table_path);
-```
+### Predicate pushdown
 
-You can use `PredicateBuilder` to build filters and pushdown them by 
`ReadContextBuilder`:
+Use `ReadContextBuilder` to configure the reader. 
`EnablePredicateFilter(true)` requests row-level
+filtering as well as any pruning supported by the reader:
 
 ```c++
-# Example filter: 'f3' > 12.0 OR 'f1' == 1
+// Example filter: 'f3' > 12.0 OR 'f1' == 1
 PAIMON_ASSIGN_OR_RAISE(
     auto predicate,
-    PredicateBuilder::Or(
-        {PredicateBuilder::GreaterThan(/*field_index=*/3, /*field_name=*/"f3",
-                                        FieldType::DOUBLE, 
Literal(static_cast<double>(12.0))),
-        PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", 
FieldType::INT,
-                                    Literal(1))}));
-ReadContextBuilder read_context_builder(table_path);
+    paimon::PredicateBuilder::Or({
+        paimon::PredicateBuilder::GreaterThan(
+            /*field_index=*/3, /*field_name=*/"f3",
+            paimon::FieldType::DOUBLE, paimon::Literal(12.0)),
+        paimon::PredicateBuilder::Equal(
+            /*field_index=*/1, /*field_name=*/"f1",
+            paimon::FieldType::INT, paimon::Literal(1))}));
+paimon::ReadContextBuilder read_context_builder(table_path);
 read_context_builder.SetPredicate(predicate).EnablePredicateFilter(true);
 ```
 
-You can also pushdown projection by `ReadContextBuilder`:
+Set the projected fields on the same read context:
 
 ```c++
-# select f3 and f2 columns
-read_context_builder.SetReadSchema({"f3", "f1", "f2"});
+// Return f3, f1, and f2, in that order
+read_context_builder.SetReadFieldNames({"f3", "f1", "f2"});
 ```
 
 ### Generate Splits
 
-Then you can step into Scan Plan stage to get `splits`:
+Create a scan plan to discover the splits to read:
 
 ```c++
 // scan
@@ -217,11 +230,12 @@ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<paimon::Plan> 
plan, scanner->CreatePlan()
 auto splits = plan->Splits();
 ```
 
-Finally, you can read data from the `splits` to arrow format.
+Pass the planned splits to a table reader to obtain Arrow batches.
 
 ### Read Apache Arrow
 
-This requires `C++ Arrow` to be installed.
+Import each returned batch into Arrow C++ objects. This example collects the 
batches in memory;
+for large results, process batches as they arrive instead.
 
 ```c++
 PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::ReadContext> read_context,
diff --git a/docs/docs/program-api/file-cache.mdx 
b/docs/docs/program-api/file-cache.mdx
index eff163ada8..8c58a398bc 100644
--- a/docs/docs/program-api/file-cache.mdx
+++ b/docs/docs/program-api/file-cache.mdx
@@ -1,6 +1,6 @@
 ---
 title: "Local Cache"
-sidebar_position: 8
+sidebar_position: 10
 ---
 
 import Tabs from '@theme/Tabs';
@@ -27,15 +27,22 @@ under the License.
 
 # Local Cache
 
-When reading files from remote storage (S3, OSS, HDFS, etc.), each seek+read 
goes over the network. Paimon provides a block-level local cache that 
transparently caches file reads, significantly reducing remote I/O for repeated 
access patterns.
+Paimon's local cache stores file blocks on disk or in memory to reduce 
repeated reads from
+storage such as S3, OSS, and HDFS. Configure it on the catalog that loads your 
tables.
 
-The cache supports two modes:
-- **Disk cache**: when `local-cache.dir` is configured, blocks are cached on 
local disk.
-- **Memory cache**: when `local-cache.dir` is not configured, blocks are 
cached in memory.
+| Mode | When it is selected | Where blocks live |
+| --- | --- | --- |
+| Memory | Cache enabled without `local-cache.dir` | Process memory |
+| Disk | Cache enabled with `local-cache.dir` | The configured local directory 
|
+
+Set an explicit size limit for your workload. The cache is disabled by 
default, and enabling it
+does not automatically cache data files: the default whitelist covers metadata 
and global indexes.
+
+![Cacheable reads check a local block cache. Hits return a cached block; 
misses fetch and cache a storage block. Other reads bypass the 
cache.](/img/program-api-local-cache.svg)
 
 ## Cached File Types
 
-The cache classifies files by type. By default, only `meta` and `global-index` 
types are cached. You can customize this via the `local-cache.whitelist` option.
+The cache classifies files by path. Set `local-cache.whitelist` to select the 
file types to cache.
 
 | File Type | Config Name | Examples | Default Cached |
 |-----------|-------------|----------|----------------|
@@ -45,7 +52,8 @@ The cache classifies files by type. By default, only `meta` 
and `global-index` t
 | DATA | data | Data files (ORC, Parquet, etc.) | No |
 | FILE_INDEX | file-index | Data-file level bloom filter, bitmap | No |
 
-All file types can be added to the whitelist. The default whitelist is 
`meta,global-index`.
+The default whitelist is `meta,global-index`. The mutable `LATEST` and 
`EARLIEST` hint files bypass
+the cache even though they are classified as metadata.
 
 ## Enable Cache
 
@@ -56,24 +64,27 @@ This is a catalog-level option. Configure it when creating 
the catalog:
 <TabItem value="java" label="Java">
 
 ```java
+import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.CatalogContext;
 import org.apache.paimon.catalog.CatalogFactory;
+import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.options.Options;
+import org.apache.paimon.table.Table;
 
 Options options = new Options();
 options.set("warehouse", "s3://my-bucket/warehouse");
 options.set("local-cache.enabled", "true");
 // optional: use disk cache by specifying a directory
 options.set("local-cache.dir", "/tmp/paimon-cache");
-// optional: customize limits
+// Set a limit for cached blocks
 options.set("local-cache.max-size", "2gb");
 options.set("local-cache.block-size", "1mb");
 
 CatalogContext context = CatalogContext.create(options);
-Catalog catalog = CatalogFactory.createCatalog(context);
-
-// All tables from this catalog will use the cache
-Table table = catalog.getTable(Identifier.create("my_db", "my_table"));
+try (Catalog catalog = CatalogFactory.createCatalog(context)) {
+    Table table = catalog.getTable(Identifier.create("my_db", "my_table"));
+    // Read table data here; eligible file reads use the cache.
+}
 ```
 
 </TabItem>
@@ -88,14 +99,14 @@ options = {
     "local-cache.enabled": "true",
     # optional: use disk cache by specifying a directory
     "local-cache.dir": "/tmp/paimon-cache",
-    # optional: customize limits
+    # Set a limit for cached blocks
     "local-cache.max-size": "2gb",
     "local-cache.block-size": "1mb",
 }
 
 catalog = pypaimon.create_catalog(options)
 
-# All tables from this catalog will use the cache
+# Eligible file reads from tables loaded by this catalog use the cache
 table = catalog.get_table("db.my_table")
 ```
 
@@ -103,28 +114,57 @@ table = catalog.get_table("db.my_table")
 
 </Tabs>
 
+The snippets assume the catalog and table already exist and storage 
credentials are configured.
+The Java snippet is a method-body fragment; declare or handle the catalog 
exceptions as in the
+[Java setup](java-api).
+
 ## Cache Options
 
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `local-cache.enabled` | Boolean | false | Whether to enable local block 
cache for file reads. |
 | `local-cache.dir` | String | (none) | Directory for storing cached blocks on 
disk. If not configured, memory cache is used. |
-| `local-cache.max-size` | MemorySize | unlimited | Maximum total size of the 
cache. When exceeded, the least recently used blocks are evicted. |
+| `local-cache.max-size` | MemorySize | See below | Maximum cached block bytes 
per cache manager. Least recently used blocks are evicted when the limit is 
exceeded. |
 | `local-cache.block-size` | MemorySize | 1 mb | Block size for caching. Files 
are logically divided into fixed-size blocks and cached independently. |
 | `local-cache.whitelist` | String | meta,global-index | Comma-separated list 
of file types to cache. Supported values: `meta`, `global-index`, 
`bucket-index`, `data`, `file-index`. |
 
+When `local-cache.max-size` is omitted, Java has no configured size limit. 
PyPaimon uses a 256 MiB
+memory limit or a 10 GiB disk limit. Set the option explicitly when you want 
the same limit across
+clients. The limit accounts for cached block bytes, not all reader buffers or 
process memory.
+
 ## How It Works
 
-- Files are logically divided into fixed-size blocks (default 1 MB).
-- On the first read, blocks are downloaded from remote storage and cached 
locally (on disk or in memory).
-- Subsequent reads of the same block are served from the local cache, skipping 
remote I/O.
-- When using disk cache, cache files are keyed by remote file path and block 
offset, so they persist across process restarts and can be reused.
-- When the cache exceeds `max-size`, the least recently used blocks are 
evicted automatically.
+1. The reader requests bytes from an eligible file. The cache maps the byte 
range to fixed-size
+   blocks, using 1 MiB blocks by default.
+2. A hit returns the cached block. A miss reads the block from storage and 
adds it to the cache.
+3. Least recently used blocks are evicted when a configured limit is exceeded.
+
+A cache hit avoids fetching that block's contents again; it does not eliminate 
all remote metadata
+operations. In Java disk mode, opening a file still obtains its status. The 
disk cache identifies a
+file by path, length, and modification time, and separates entries by block 
size. Existing entries
+can be reused after restart when this identity matches. Memory caches do not 
survive process restarts.
+
+Caching applies to reads. It does not change commit semantics or replace 
durable table storage.
 
 ## Cache Lifecycle
 
-The cache is created and managed by the Catalog. All tables obtained from the 
same catalog share a single cache instance. The cache lives as long as the 
Catalog object is reachable — no explicit close is needed.
+### Java applications and distributed workers
+
+A catalog creates the cache used by the tables it loads. After a 
catalog-created `FileIO` is
+serialized and deserialized on a worker, it retains its cache configuration 
and lazily creates or
+reuses a cache manager in that worker JVM.
+
+Deserialized wrappers with matching directory, maximum size, and block size 
share a JVM-local cache
+manager and its size limit. Memory entries remain isolated by the originating 
wrapper's namespace.
+The limit is local to that manager, not a cluster-wide budget. In disk mode 
the directory must be
+available and writable on each worker that uses it.
+
+Close readers and other owned resources when finished. Closing a deserialized 
`FileIO` releases its
+reference to the shared manager; the last reference releases the shared entry. 
A manually constructed
+`CachingFileIO` without serialized cache configuration cannot recreate a cache 
after deserialization.
 
-In distributed computing frameworks (Flink, Spark), the `FileIO` is serialized 
and shipped to task managers. After deserialization, the cache is **not** 
recreated — reads fall through directly to the remote storage. This is by 
design: the cache lifecycle is bound to the Catalog that created it, and a 
deserialized `FileIO` is no longer managed by any Catalog.
+### PyPaimon workers
 
-If you need caching on task managers, create a new Catalog with cache options 
enabled on each worker node.
+PyPaimon's cache is removed when `CachingFileIO` is pickled. An unpickled copy 
reads directly from
+storage. Create a catalog with cache options on each Python worker when you 
need caching there.
+The Java worker lifecycle above does not apply to Python serialization.
diff --git a/docs/docs/program-api/flink-api.mdx 
b/docs/docs/program-api/flink-api.mdx
index 2ac1775a85..20010aa507 100644
--- a/docs/docs/program-api/flink-api.mdx
+++ b/docs/docs/program-api/flink-api.mdx
@@ -1,6 +1,6 @@
 ---
 title: "Flink API"
-sidebar_position: 2
+sidebar_position: 6
 ---
 
 import Stable from '@site/src/components/Stable';
@@ -27,11 +27,19 @@ under the License.
 
 # Flink API
 
-:::info
+Use the Flink builders to connect a `DataStream` to a Paimon table. The sink 
integrates writer
+routing, checkpoints, and commits with Flink. Use `RichCdcSinkBuilder` when 
incoming records also
+carry schema changes.
 
-If possible, recommend using Flink SQL or Spark SQL, or simply use SQL APIs in 
programs.
+| Input or task | API |
+| --- | --- |
+| Flink `Row` values with a known schema | `FlinkSinkBuilder.forRow` |
+| Flink internal `RowData` values | `FlinkSinkBuilder.forRowData` |
+| Read a table as a `DataStream<Row>` | `FlinkSourceBuilder.buildForRow` |
+| Ingest typed CDC records with schema evolution | `RichCdcSinkBuilder` |
 
-:::
+For SQL transformations, you can also convert between DataStream and the Table 
API.
+See [DataStream API 
Integration](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/table/data_stream_api/).
 
 ## Dependency
 
@@ -64,14 +72,21 @@ Or download the jar file:
 
 </Unstable>
 
-Please choose your Flink version.
+Match the Paimon artifact and Flink dependencies to the Flink version you run. 
See
+[Flink installation](../flink/installation) for connector and Hadoop runtime 
setup.
 
-Paimon relies on Hadoop environment, you should add hadoop classpath or 
bundled jar.
+## Prepare a table
 
-Not only DataStream API, you can also read or write to Paimon tables by the 
conversion between DataStream and Table in Flink.
-See [DataStream API 
Integration](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/table/data_stream_api/).
+The read and write examples use `my_db.my_table` from the [Java API 
setup](java-api#create-table),
+with columns `f0 STRING` and `f1 INT`, primary key `f0`, and two fixed 
buckets. Use the same warehouse
+in each example. In a cluster, choose a 
[filesystem](../maintenance/filesystems) accessible to all
+workers instead of the local path shown here.
+
+## Write to Table
 
-## Write to Table 
+Declare the input's field names and types in the same order as the Paimon 
schema. The example
+updates Alice's value from 12 to 100. Enable checkpointing for an unbounded 
streaming source so
+that the sink can publish data as checkpoints complete.
 
 ```java
 import org.apache.paimon.catalog.Catalog;
@@ -92,10 +107,9 @@ import org.apache.flink.types.RowKind;
 public class WriteToTable {
 
     public static void writeTo() throws Exception {
-        // create environments of both APIs
+        // Create the Flink execution environment.
         StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
-        // for CONTINUOUS_UNBOUNDED source, set checkpoint interval
-        // env.enableCheckpointing(60_000);
+        env.enableCheckpointing(60_000);
 
         // create a changelog DataStream
         DataStream<Row> input =
@@ -106,34 +120,35 @@ public class WriteToTable {
                                 Row.ofKind(RowKind.UPDATE_AFTER, "Alice", 100))
                         .returns(
                                 Types.ROW_NAMED(
-                                        new String[] {"name", "age"}, 
Types.STRING, Types.INT));
+                                        new String[] {"f0", "f1"}, 
Types.STRING, Types.INT));
 
         // get table from catalog
         Options catalogOptions = new Options();
-        catalogOptions.set("warehouse", "/path/to/warehouse");
-        Catalog catalog = 
FlinkCatalogFactory.createPaimonCatalog(catalogOptions);
-        Table table = catalog.getTable(Identifier.create("my_db", "T"));
-
-        DataType inputType =
-                DataTypes.ROW(
-                        DataTypes.FIELD("name", DataTypes.STRING()),
-                        DataTypes.FIELD("age", DataTypes.INT()));
-        FlinkSinkBuilder builder = new FlinkSinkBuilder(table).forRow(input, 
inputType);
-
-        // set sink parallelism
-        // builder.parallelism(_your_parallelism)
-
-        // set overwrite mode
-        // builder.overwrite(...)
-
-        builder.build();
-        env.execute();
+        catalogOptions.set("warehouse", "file:///tmp/paimon-api-warehouse");
+        try (Catalog catalog = 
FlinkCatalogFactory.createPaimonCatalog(catalogOptions)) {
+            Table table = catalog.getTable(Identifier.create("my_db", 
"my_table"));
+
+            DataType inputType =
+                    DataTypes.ROW(
+                            DataTypes.FIELD("f0", DataTypes.STRING()),
+                            DataTypes.FIELD("f1", DataTypes.INT()));
+            FlinkSinkBuilder builder = new 
FlinkSinkBuilder(table).forRow(input, inputType);
+
+            builder.build();
+            env.execute();
+        }
     }
 }
 ```
 
+Use `builder.parallelism(...)` to set sink parallelism. Select 
`builder.overwrite()` only for an
+intentional overwrite; see [write semantics](../flink/sql-write).
+
 ## Read from Table
 
+This example performs a bounded read of the current table state. Use 
`sourceBounded(false)` for a
+continuous source and choose the appropriate [streaming read 
mode](../flink/sql-query).
+
 ```java
 import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.Identifier;
@@ -145,55 +160,66 @@ import org.apache.paimon.table.Table;
 import org.apache.flink.streaming.api.datastream.DataStream;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
 import org.apache.flink.types.Row;
+import org.apache.flink.util.CloseableIterator;
 
 public class ReadFromTable {
 
     public static void readFrom() throws Exception {
-        // create environments of both APIs
+        // Create the Flink execution environment.
         StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
 
         // get table from catalog
         Options catalogOptions = new Options();
-        catalogOptions.set("warehouse", "/path/to/warehouse");
-        Catalog catalog = 
FlinkCatalogFactory.createPaimonCatalog(catalogOptions);
-        Table table = catalog.getTable(Identifier.create("my_db", "T"));
-
-        // table = 
table.copy(Collections.singletonMap("scan.file-creation-time-millis", "..."));
-        
-        FlinkSourceBuilder builder = new FlinkSourceBuilder(table).env(env);
-        
-        // builder.sourceBounded(true);
-        // builder.projection(...);
-        // builder.predicate(...);
-        // builder.limit(...);
-        // builder.sourceParallelism(...);
-
-        DataStream<Row> dataStream = builder.buildForRow();
-
-        // use this datastream
-        dataStream.executeAndCollect().forEachRemaining(System.out::println);
-
-        // prints:
-        // +I[Bob, 12]
-        // +I[Alice, 12]
-        // -U[Alice, 12]
-        // +U[Alice, 14]
+        catalogOptions.set("warehouse", "file:///tmp/paimon-api-warehouse");
+        try (Catalog catalog = 
FlinkCatalogFactory.createPaimonCatalog(catalogOptions)) {
+            Table table = catalog.getTable(Identifier.create("my_db", 
"my_table"));
+
+            FlinkSourceBuilder builder = new FlinkSourceBuilder(table)
+                    .env(env)
+                    .sourceBounded(true);
+
+            DataStream<Row> dataStream = builder.buildForRow();
+
+            try (CloseableIterator<Row> rows = dataStream.executeAndCollect()) 
{
+                rows.forEachRemaining(System.out::println);
+            }
+        }
     }
 }
 ```
 
-## Cdc ingestion Table
+After running the write example against the fresh sample table, the bounded 
result contains
+Alice with value 100 and Bob with value 5. Output order is not guaranteed. A 
continuous read's
+row kinds depend on the table's [changelog 
configuration](../primary-key-table/changelog-producer).
+
+Use `projection`, `predicate`, `limit`, and `sourceParallelism` on the source 
builder to customize
+the scan. Predicate field indexes refer to the table schema.
+
+## CDC ingestion with schema evolution {#cdc-ingestion-table}
+
+`RichCdcRecord` carries field names, types, and string-encoded values. 
`RichCdcSinkBuilder` uses the
+catalog loader to apply schema changes and write the records. This also 
supports adding columns
+to a [partial-update table](../primary-key-table/merge-engine/partial-update).
+
+Create this separate table first, using a Flink SQL catalog pointing to the 
same warehouse:
 
-Paimon supports ingest data into Paimon tables with schema evolution.
-- You can use Java API to write cdc records into Paimon Tables.
-- You can write records to Paimon's partial-update table with adding columns 
dynamically.
+```sql
+CREATE DATABASE IF NOT EXISTS my_db;
+CREATE TABLE my_db.cdc_orders (
+    order_id BIGINT,
+    price DOUBLE,
+    PRIMARY KEY (order_id) NOT ENFORCED
+) WITH ('bucket' = '2');
+```
 
-Here is an example to use `RichCdcSinkBuilder` API:
+The second record below introduces `dt`. Supply a serializable `CatalogLoader` 
so runtime operators
+can access the same catalog and evolve the schema.
 
 ```java
+import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.CatalogLoader;
-import org.apache.paimon.flink.FlinkCatalogFactory;
 import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.flink.FlinkCatalogFactory;
 import org.apache.paimon.flink.sink.cdc.RichCdcRecord;
 import org.apache.paimon.flink.sink.cdc.RichCdcSinkBuilder;
 import org.apache.paimon.options.Options;
@@ -209,8 +235,7 @@ public class WriteCdcToTable {
 
     public static void writeTo() throws Exception {
         StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
-        // for CONTINUOUS_UNBOUNDED source, set checkpoint interval
-        // env.enableCheckpointing(60_000);
+        env.enableCheckpointing(60_000);
 
         DataStream<RichCdcRecord> dataStream =
                 env.fromElements(
@@ -225,20 +250,22 @@ public class WriteCdcToTable {
                                 .field("dt", DataTypes.TIMESTAMP(), 
"2023-06-12 20:21:12")
                                 .build());
 
-        Identifier identifier = Identifier.create("my_db", "T");
+        Identifier identifier = Identifier.create("my_db", "cdc_orders");
         Options catalogOptions = new Options();
-        catalogOptions.set("warehouse", "/path/to/warehouse");
+        catalogOptions.set("warehouse", "file:///tmp/paimon-api-warehouse");
         CatalogLoader catalogLoader =
                 () -> FlinkCatalogFactory.createPaimonCatalog(catalogOptions);
-        Table table = catalogLoader.load().getTable(identifier);
+        try (Catalog catalog = catalogLoader.load()) {
+            Table table = catalog.getTable(identifier);
 
-        new RichCdcSinkBuilder(table)
-                .forRichCdcRecord(dataStream)
-                .identifier(identifier)
-                .catalogLoader(catalogLoader)
-                .build();
+            new RichCdcSinkBuilder(table)
+                    .forRichCdcRecord(dataStream)
+                    .identifier(identifier)
+                    .catalogLoader(catalogLoader)
+                    .build();
 
-        env.execute();
+            env.execute();
+        }
     }
 }
-```
\ No newline at end of file
+```
diff --git a/docs/docs/program-api/index.md b/docs/docs/program-api/index.md
index 59c775d0b4..890a42ea1e 100644
--- a/docs/docs/program-api/index.md
+++ b/docs/docs/program-api/index.md
@@ -21,3 +21,46 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 -->
+
+# Program API
+
+Use Paimon's program APIs to manage catalogs, embed table reads and writes in 
an application,
+or build a connector for a processing engine. Start with the interface that 
matches your application.
+
+## Choose an API
+
+| What you want to do | Start here |
+| --- | --- |
+| Read and write tables in a standalone Java application | [Java 
API](java-api) |
+| Create databases and tables, or change schemas | [Catalog API](catalog-api) |
+| Build a Flink DataStream job or ingest records with schema evolution | 
[Flink API](flink-api) |
+| Call a REST catalog from a lightweight Java client | [REST Java 
Client](rest-api) |
+| Integrate a native C++ engine | [C++ API](cpp-api) |
+| Access Paimon from Rust | [Rust API](rust-api) |
+| Work with Python, Arrow, or AI datasets | [PyPaimon](../pypaimon/) |
+| Reduce repeated file reads | [Local Cache](file-cache) |
+
+For SQL applications, start with [Flink](../flink/quick-start) or 
[Spark](../spark/quick-start).
+These integrations handle execution, data distribution, and recovery for you.
+
+![Program APIs: catalogs manage metadata, table APIs plan reads and prepare 
writes, and engines coordinate execution.](/img/program-api-overview.svg)
+
+## Follow the Java workflow
+
+1. **Set up the client.** Add the [dependency and create a 
catalog](java-api#dependency).
+2. **Create or load a table.** Define its schema through the [Catalog 
API](catalog-api).
+3. **Read or write data.** Follow [Java Reads](java-reading) or [Java 
Writes](java-writing)
+   for batch and streaming examples.
+4. **Integrate with your runtime.** Distribute splits and writer input, close 
resources, and
+   coordinate checkpoints and commits. Use [types and predicates](java-types) 
to convert records
+   and construct filters.
+
+## Understand the boundaries
+
+A **catalog** resolves table names and manages metadata. A **table** supplies 
builders for reads
+and writes. A **scan** plans splits; readers consume those splits. Writers 
prepare file changes;
+a committer publishes them in snapshots.
+
+The low-level Java API exposes these building blocks. A custom distributed 
application must provide
+scheduling, writer routing, and recovery. The [Flink builders](flink-api) 
connect Paimon to Flink's runtime.
+The [REST Java client](rest-api) handles catalog requests; use a table API to 
read or write rows.
diff --git a/docs/docs/program-api/java-api.mdx 
b/docs/docs/program-api/java-api.mdx
index 0f58070037..b03de1c982 100644
--- a/docs/docs/program-api/java-api.mdx
+++ b/docs/docs/program-api/java-api.mdx
@@ -1,6 +1,6 @@
 ---
 title: "Java API"
-sidebar_position: 3
+sidebar_position: 1
 ---
 
 import Stable from '@site/src/components/Stable';
@@ -27,11 +27,17 @@ under the License.
 
 # Java API
 
-:::info
+Use the Java API to access Paimon tables without a processing engine, or to 
implement an engine
+integration. This page sets up a catalog and a sample table used by the 
reading and writing guides.
 
-If possible, recommend using computing engines such as Flink SQL or Spark SQL.
+| Task | Guide |
+| --- | --- |
+| Manage databases, tables, and schemas | [Catalog API](catalog-api) |
+| Plan splits, filter rows, and follow new snapshots | [Java 
Reads](java-reading) |
+| Prepare and commit batch or streaming writes | [Java Writes](java-writing) |
+| Convert row values and build predicates | [Types and Predicates](java-types) 
|
 
-:::
+For a Flink application, use the [Flink API](flink-api) so the engine can 
manage checkpoints and commits.
 
 ## Dependency
 
@@ -57,11 +63,14 @@ Or download the jar file:
 
 </Unstable>
 
-Paimon relies on Hadoop environment, you should add hadoop classpath or 
bundled jar.
+Add the Hadoop libraries required by your environment to the runtime 
classpath. For remote storage,
+configure the corresponding [filesystem dependencies and 
credentials](../maintenance/filesystems).
 
 ## Create Catalog
 
-Before coming into contact with the Table, you need to create a Catalog.
+`CatalogFactory` selects a catalog from the supplied options. The helper below 
uses a local
+filesystem warehouse. Save it as `CreateCatalog.java` alongside the example 
classes, and change the
+warehouse path for your environment.
 
 ```java
 import org.apache.paimon.catalog.Catalog;
@@ -73,29 +82,31 @@ import org.apache.paimon.options.Options;
 public class CreateCatalog {
 
     public static Catalog createFilesystemCatalog() {
-        CatalogContext context = CatalogContext.create(new Path("..."));
+        CatalogContext context =
+                CatalogContext.create(new 
Path("file:///tmp/paimon-api-warehouse"));
         return CatalogFactory.createCatalog(context);
     }
 
     public static Catalog createHiveCatalog() {
-        // Paimon Hive catalog relies on Hive jars
-        // You should add hive classpath or hive bundled jar.
+        // Add the Hive client libraries and configure these paths for your 
environment.
         Options options = new Options();
-        options.set("warehouse", "...");
+        options.set("warehouse", "hdfs:///path/to/warehouse");
         options.set("metastore", "hive");
-        options.set("uri", "...");
-        options.set("hive-conf-dir", "...");
-        options.set("hadoop-conf-dir", "...");
-        CatalogContext context = CatalogContext.create(options);
-        return CatalogFactory.createCatalog(context);
+        options.set("uri", "thrift://localhost:9083");
+        options.set("hive-conf-dir", "/path/to/hive/conf");
+        options.set("hadoop-conf-dir", "/path/to/hadoop/conf");
+        return CatalogFactory.createCatalog(CatalogContext.create(options));
     }
 }
 ```
 
+Keep the catalog open while using its tables, and close it when the 
application finishes.
+See [Catalogs](../concepts/catalog) for other catalog implementations and 
their configuration.
+
 ## Create Table
 
-You can use the catalog to create tables. The created tables are persistence 
in the file system.
-Next time you can directly obtain these tables.
+Run this example once in a fresh warehouse. It creates `my_db.my_table` with a 
string primary key
+`f0`, an integer value `f1`, and two fixed buckets. The following guides use 
this same schema.
 
 ```java
 import org.apache.paimon.catalog.Catalog;
@@ -105,376 +116,75 @@ import org.apache.paimon.types.DataTypes;
 
 public class CreateTable {
 
-    public static void main(String[] args) {
-        Schema.Builder schemaBuilder = Schema.newBuilder();
-        schemaBuilder.primaryKey("f0", "f1");
-        schemaBuilder.partitionKeys("f1");
-        schemaBuilder.column("f0", DataTypes.STRING());
-        schemaBuilder.column("f1", DataTypes.INT());
-        Schema schema = schemaBuilder.build();
-
-        Identifier identifier = Identifier.create("my_db", "my_table");
-        try {
-            Catalog catalog = CreateCatalog.createFilesystemCatalog();
-            catalog.createTable(identifier, schema, false);
-        } catch (Catalog.TableAlreadyExistException e) {
-            // do something
-        } catch (Catalog.DatabaseNotExistException e) {
-            // do something
+    public static void main(String[] args) throws Exception {
+        Schema schema = Schema.newBuilder()
+                .column("f0", DataTypes.STRING().notNull())
+                .column("f1", DataTypes.INT())
+                .primaryKey("f0")
+                .option("bucket", "2")
+                .build();
+
+        try (Catalog catalog = CreateCatalog.createFilesystemCatalog()) {
+            catalog.createDatabase("my_db", true);
+            catalog.createTable(Identifier.create("my_db", "my_table"), 
schema, false);
         }
     }
 }
 ```
 
+`true` in `createDatabase` ignores an existing database; `false` in 
`createTable` reports an
+existing table. This avoids silently reusing a table with a different schema.
+For an append table, omit the primary key and choose the appropriate
+[append table layout](../append-table/).
+
 ## Get Table
 
-The `Table` interface provides access to the table metadata and tools to read 
and write table.
+Load the table by identifier within the catalog's lifetime:
 
 ```java
 import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.table.Table;
 
-public class GetTable {
-
-    public static Table getTable() {
-        Identifier identifier = Identifier.create("my_db", "my_table");
-        try {
-            Catalog catalog = CreateCatalog.createFilesystemCatalog();
-            return catalog.getTable(identifier);
-        } catch (Catalog.TableNotExistException e) {
-            // do something
-            throw new RuntimeException("table not exist");
-        }
-    }
-}
-```
-
-## Batch Read
-
-For relatively small amounts of data, or for data that has undergone 
projection and filtering,
-you can directly use a standalone program to read the table data.
-
-But if the data volume of the table is relatively large, you can distribute 
splits to different tasks for reading.
-
-The reading is divided into two stages:
-
-1. Scan Plan: Generate plan splits in a global node ('Coordinator', or named 
'Driver').
-2. Read Split: Read split in distributed tasks.
-
-```java
-import org.apache.paimon.data.InternalRow;
-import org.apache.paimon.predicate.Predicate;
-import org.apache.paimon.predicate.PredicateBuilder;
-import org.apache.paimon.reader.RecordReader;
-import org.apache.paimon.table.Table;
-import org.apache.paimon.table.source.ReadBuilder;
-import org.apache.paimon.table.source.Split;
-import org.apache.paimon.table.source.TableRead;
-import org.apache.paimon.types.DataTypes;
-import org.apache.paimon.types.RowType;
-
-import com.google.common.collect.Lists;
-
-import java.util.List;
-
-public class ReadTable {
-
-    public static void main(String[] args) throws Exception {
-        // 1. Create a ReadBuilder and push filter (`withFilter`)
-        // and projection (`withProjection`) if necessary
-        Table table = GetTable.getTable();
-
-        PredicateBuilder builder =
-                new PredicateBuilder(RowType.of(DataTypes.STRING(), 
DataTypes.INT()));
-        Predicate notNull = builder.isNotNull(0);
-        Predicate greaterOrEqual = builder.greaterOrEqual(1, 12);
-
-        int[] projection = new int[]{0, 1};
-
-        ReadBuilder readBuilder =
-                table.newReadBuilder()
-                        .withProjection(projection)
-                        .withFilter(Lists.newArrayList(notNull, 
greaterOrEqual));
-
-        // 2. Plan splits in 'Coordinator' (or named 'Driver')
-        List<Split> splits = readBuilder.newScan().plan().splits();
-
-        // 3. Distribute these splits to different tasks
-
-        // 4. Read a split in task
-        // You can use executeFilter to do filter per record.
-        // By default, only capable of performing coarse-grained filtering.
-        TableRead read = readBuilder.newRead().executeFilter();
-        RecordReader<InternalRow> reader = read.createReader(splits);
-        reader.forEachRemaining(System.out::println);
-    }
-}
-```
-
-### Adjust Read Batch Size at Runtime
-
-Parquet and ORC readers can share a `ReadBatchSizer` to adjust the row count 
and vector
-capacity of future physical batches without recreating readers:
-
-```java
-import org.apache.paimon.reader.ReadBatchSizer;
-import org.apache.paimon.table.source.TableRead;
-
-ReadBatchSizer sizer = new ReadBatchSizer();
-TableRead read = readBuilder.newRead().withReadBatchSizer(sizer);
-RecordReader<InternalRow> reader = read.createReader(splits);
-
-sizer.setBatchSize(256);
-```
-
-Configure the sizer on `TableRead` before creating readers. A newly created 
sizer has no batch size,
-so readers initially use their configured default. Every explicitly set batch 
size must be positive.
-Call `clearBatchSize()` to make future physical batches use the configured 
default again.
-
-A supporting reader snapshots the batch size before starting a physical batch. 
If the size has
-changed, it replaces an idle reusable batch with vectors sized for the new 
value and then starts the
-read. The allocation is reused until the batch size changes again. 
Consequently, lowering the
-batch size reduces the vector capacity of future batches instead of only 
changing their logical
-row count.
-
-An update never mutates a batch that has already started or is still owned by 
a consumer.
-Asynchronously prefetched batches may therefore retain the previous size. For 
a pooled ORC reader,
-each idle pool entry adopts the current size the next time it is acquired, 
while in-flight entries
-keep their old vectors until released. During such a transition, old and new 
vectors can coexist.
-
-The sizer uses latest-value semantics: when updates happen faster than 
physical batches start,
-readers may skip intermediate batch sizes. Engines should avoid changing the 
size too frequently
-because each observed size change reallocates vectors and can add allocation 
and garbage-collection
-overhead. A hysteresis interval or minimum adjustment period is recommended.
-
-For concurrent scans, estimate memory using the selected batch size multiplied 
by the number of
-active and prefetched batches. This allows an engine to reduce future batch 
capacities under memory
-pressure and grow them again when more memory is available.
-
-## Batch Write
-
-The writing is divided into two stages:
-
-1. Write records: Write records in distributed tasks, generate commit messages.
-2. Commit/Abort: Collect all CommitMessages, commit them in a global node 
('Coordinator', or named 'Driver', or named 'Committer').
-   When the commit fails for certain reason, abort unsuccessful commit via 
CommitMessages.
-
-```java
-import org.apache.paimon.data.BinaryString;
-import org.apache.paimon.data.GenericRow;
-import org.apache.paimon.table.Table;
-import org.apache.paimon.table.sink.BatchTableCommit;
-import org.apache.paimon.table.sink.BatchTableWrite;
-import org.apache.paimon.table.sink.BatchWriteBuilder;
-import org.apache.paimon.table.sink.CommitMessage;
-
-import java.util.List;
-
-public class BatchWrite {
-    public static void main(String[] args) throws Exception {
-        // 1. Create a WriteBuilder (Serializable)
-        Table table = GetTable.getTable();
-        BatchWriteBuilder writeBuilder = 
table.newBatchWriteBuilder().withOverwrite();
-
-        // 2. Write records in distributed tasks
-        BatchTableWrite write = writeBuilder.newWrite();
-
-        GenericRow record1 = GenericRow.of(BinaryString.fromString("Alice"), 
12);
-        GenericRow record2 = GenericRow.of(BinaryString.fromString("Bob"), 5);
-        GenericRow record3 = GenericRow.of(BinaryString.fromString("Emily"), 
18);
-
-        // If this is a distributed write, you can use 
writeBuilder.newWriteSelector.
-        // WriteSelector determines to which logical downstream writers a 
record should be written to.
-        // If it returns empty, no data distribution is required.
-
-        write.write(record1);
-        write.write(record2);
-        write.write(record3);
-
-        List<CommitMessage> messages = write.prepareCommit();
-
-        // 3. Collect all CommitMessages to a global node and commit
-        BatchTableCommit commit = writeBuilder.newCommit();
-        commit.commit(messages);
-
-        // Abort unsuccessful commit to delete data files
-        // commit.abort(messages);
-    }
+try (Catalog catalog = CreateCatalog.createFilesystemCatalog()) {
+    Table table = catalog.getTable(Identifier.create("my_db", "my_table"));
+    // Create read or write builders here.
 }
 ```
 
-## Stream Read
+Next, run the [batch write example](java-writing#batch-write), followed by the
+[batch read example](java-reading#batch-read). Catalog operations and examples 
that show a complete
+class declare `throws Exception` for brevity; applications should handle 
failures at their own
+error-handling boundary.
 
-The difference of Stream Read is that StreamTableScan can continuously scan 
and generate splits.
+## Read and write data
 
-StreamTableScan provides the ability to checkpoint and restore, which can let 
you save the correct state
-during stream reading.
+### Batch Read
 
-```java
-import org.apache.paimon.data.InternalRow;
-import org.apache.paimon.predicate.Predicate;
-import org.apache.paimon.predicate.PredicateBuilder;
-import org.apache.paimon.reader.RecordReader;
-import org.apache.paimon.table.Table;
-import org.apache.paimon.table.source.ReadBuilder;
-import org.apache.paimon.table.source.Split;
-import org.apache.paimon.table.source.StreamTableScan;
-import org.apache.paimon.table.source.TableRead;
-import org.apache.paimon.types.DataTypes;
-import org.apache.paimon.types.RowType;
+See [Java Reads: Batch Read](java-reading#batch-read) for split planning, 
projection, and row filtering.
 
-import com.google.common.collect.Lists;
+#### Adjust Read Batch Size at Runtime
 
-import java.util.List;
+See [runtime batch sizing](java-reading#adjust-read-batch-size-at-runtime) for 
Parquet and ORC readers.
 
-public class StreamReadTable {
+### Batch Write
 
-    public static void main(String[] args) throws Exception {
-        // 1. Create a ReadBuilder and push filter (`withFilter`)
-        // and projection (`withProjection`) if necessary
-        Table table = GetTable.getTable();
-
-        PredicateBuilder builder =
-                new PredicateBuilder(RowType.of(DataTypes.STRING(), 
DataTypes.INT()));
-        Predicate notNull = builder.isNotNull(0);
-        Predicate greaterOrEqual = builder.greaterOrEqual(1, 12);
-
-        int[] projection = new int[]{0, 1};
-
-        ReadBuilder readBuilder =
-                table.newReadBuilder()
-                        .withProjection(projection)
-                        .withFilter(Lists.newArrayList(notNull, 
greaterOrEqual));
-
-        // 2. Plan splits in 'Coordinator' (or named 'Driver')
-        StreamTableScan scan = readBuilder.newStreamScan();
-        while (true) {
-            List<Split> splits = scan.plan().splits();
-            // Distribute these splits to different tasks
-
-            Long state = scan.checkpoint();
-            // can be restored in scan.restore(state) after fail over
-
-            // 3. Read a split in task
-            // You can use executeFilter to do filter per record.
-            // By default, only capable of performing coarse-grained filtering.
-            TableRead read = readBuilder.newRead().executeFilter();
-            RecordReader<InternalRow> reader = read.createReader(splits);
-            reader.forEachRemaining(System.out::println);
-
-            Thread.sleep(1000);
-        }
-    }
-}
-```
+See [Java Writes: Batch Write](java-writing#batch-write) for preparing and 
committing a batch.
 
-## Stream Write
+### Stream Read
 
-The difference of Stream Write is that StreamTableCommit can continuously 
commit.
+See [Java Reads: Stream Read](java-reading#stream-read) for continuous 
planning and checkpoint recovery.
 
-Key points to achieve exactly-once consistency:
+### Stream Write
 
-- CommitUser represents a user. A user can commit multiple times. In 
distributed processing, you are
-  expected to use the same commitUser.
-- Different applications need to use different commitUsers.
-- The commitIdentifier of `StreamTableWrite` and `StreamTableCommit` needs to 
be consistent, and the
-  id needs to be incremented for the next committing.
-- When a failure occurs, if you still have uncommitted `CommitMessage`s, 
please use `StreamTableCommit#filterAndCommit`
-  to exclude the committed messages by commitIdentifier.
+See [Java Writes: Stream Write](java-writing#stream-write) for commit 
identities and retry handling.
 
-```java
-import org.apache.paimon.data.BinaryString;
-import org.apache.paimon.data.GenericRow;
-import org.apache.paimon.table.Table;
-import org.apache.paimon.table.sink.CommitMessage;
-import org.apache.paimon.table.sink.StreamTableCommit;
-import org.apache.paimon.table.sink.StreamTableWrite;
-import org.apache.paimon.table.sink.StreamWriteBuilder;
+## Reference
 
-import java.util.List;
+### Data Types
 
-public class StreamWriteTable {
+See the [internal value mapping](java-types#data-types) before constructing 
`InternalRow` values.
 
-    public static void main(String[] args) throws Exception {
-        // 1. Create a WriteBuilder (Serializable)
-        Table table = GetTable.getTable();
-        StreamWriteBuilder writeBuilder = table.newStreamWriteBuilder();
-
-        // 2. Write records in distributed tasks
-        StreamTableWrite write = writeBuilder.newWrite();
-        // commitIdentifier like Flink checkpointId
-        long commitIdentifier = 0;
-
-        while (true) {
-            GenericRow record1 = 
GenericRow.of(BinaryString.fromString("Alice"), 12);
-            GenericRow record2 = GenericRow.of(BinaryString.fromString("Bob"), 
5);
-            GenericRow record3 = 
GenericRow.of(BinaryString.fromString("Emily"), 18);
-
-            // If this is a distributed write, you can use 
writeBuilder.newWriteSelector.
-            // WriteSelector determines to which logical downstream writers a 
record should be written to.
-            // If it returns empty, no data distribution is required.
-
-            write.write(record1);
-            write.write(record2);
-            write.write(record3);
-            List<CommitMessage> messages = write.prepareCommit(false, 
commitIdentifier);
-            commitIdentifier++;
-
-            // 3. Collect all CommitMessages to a global node and commit
-            StreamTableCommit commit = writeBuilder.newCommit();
-            commit.commit(commitIdentifier, messages);
-
-            // 4. When failure occurs and you're not sure if the commit 
process is successful,
-            //    you can use `filterAndCommit` to retry the commit process.
-            //    Succeeded commits will be automatically skipped.
-            /*
-            Map<Long, List<CommitMessage>> commitIdentifiersAndMessages = new 
HashMap<>();
-            commitIdentifiersAndMessages.put(commitIdentifier, messages);
-            commit.filterAndCommit(commitIdentifiersAndMessages);
-            */
-
-            Thread.sleep(1000);
-        }
-    }
-}
-```
+### Predicate Types
 
-## Data Types
-
-| Java         | Paimon                               | 
-|:-------------|:-------------------------------------|
-| boolean      | boolean                              |
-| byte         | byte                                 |
-| short        | short                                |
-| int          | int                                  |
-| long         | long                                 |
-| float        | float                                |
-| double       | double                               |
-| string       | org.apache.paimon.data.BinaryString  |
-| decimal      | org.apache.paimon.data.Decimal       |
-| timestamp    | org.apache.paimon.data.Timestamp     |
-| byte[]       | byte[]                               |
-| array        | org.apache.paimon.data.InternalArray |
-| map          | org.apache.paimon.data.InternalMap   |
-| InternalRow  | org.apache.paimon.data.InternalRow   |
-
-## Predicate Types
-
-| SQL Predicate | Paimon Predicate                                            
| 
-|:--------------|:------------------------------------------------------------|
-| and           | org.apache.paimon.predicate.PredicateBuilder.and            |
-| or            | org.apache.paimon.predicate.PredicateBuilder.or             |
-| is null       | org.apache.paimon.predicate.PredicateBuilder.isNull         |
-| is not null   | org.apache.paimon.predicate.PredicateBuilder.isNotNull      |
-| in            | org.apache.paimon.predicate.PredicateBuilder.in             |
-| not in        | org.apache.paimon.predicate.PredicateBuilder.notIn          |
-| =             | org.apache.paimon.predicate.PredicateBuilder.equal          |
-| \<>            | org.apache.paimon.predicate.PredicateBuilder.notEqual       
|
-| \<             | org.apache.paimon.predicate.PredicateBuilder.lessThan       
|
-| \<=            | org.apache.paimon.predicate.PredicateBuilder.lessOrEqual    
|
-| >             | org.apache.paimon.predicate.PredicateBuilder.greaterThan    |
-| >=            | org.apache.paimon.predicate.PredicateBuilder.greaterOrEqual |
-| between       | org.apache.paimon.predicate.PredicateBuilder.between        |
-| like          | org.apache.paimon.predicate.PredicateBuilder.like           |
-| array contains | org.apache.paimon.predicate.PredicateBuilder.arrayContains |
+See the [predicate reference](java-types#predicate-types) for supported filter 
builders.
diff --git a/docs/docs/program-api/java-reading.md 
b/docs/docs/program-api/java-reading.md
new file mode 100644
index 0000000000..c3cedcd84f
--- /dev/null
+++ b/docs/docs/program-api/java-reading.md
@@ -0,0 +1,205 @@
+---
+title: "Java Reads"
+sidebar_position: 3
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Java Reads
+
+A read has two stages: a scan plans **splits**, and readers consume those 
splits. A standalone
+application can do both. A distributed engine plans centrally and assigns each 
split to a reader task.
+
+The examples use `CreateCatalog` and `my_db.my_table` from the [Java API 
setup](java-api).
+Populate the table with the [batch write example](java-writing#batch-write) 
first.
+
+![A coordinator plans splits with ReadBuilder; worker readers consume assigned 
splits using the same filter and projection.](/img/program-api-read-flow.svg)
+
+## Batch Read
+
+Build predicates against the table's full row type. Filter field indexes refer 
to that schema;
+projection selects the columns returned by the reader. This example returns 
`f0` and `f1` for
+records whose `f0` is not null and whose `f1` is at least 12.
+
+```java
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.predicate.PredicateBuilder;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.source.ReadBuilder;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.TableRead;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ReadTable {
+
+    public static void main(String[] args) throws Exception {
+        try (Catalog catalog = CreateCatalog.createFilesystemCatalog()) {
+            Table table = catalog.getTable(Identifier.create("my_db", 
"my_table"));
+            PredicateBuilder predicates = new 
PredicateBuilder(table.rowType());
+            ReadBuilder readBuilder = table.newReadBuilder()
+                    .withFilter(Arrays.asList(
+                            predicates.isNotNull(0), 
predicates.greaterOrEqual(1, 12)))
+                    .withProjection(new int[] {0, 1});
+
+            // Plan once in the coordinator; assign splits to workers if 
distributed.
+            List<Split> splits = readBuilder.newScan().plan().splits();
+            TableRead read = readBuilder.newRead().executeFilter();
+            try (RecordReader<InternalRow> reader = read.createReader(splits)) 
{
+                reader.forEachRemaining(row ->
+                        System.out.println(row.getString(0) + ", " + 
row.getInt(1)));
+            }
+        }
+    }
+}
+```
+
+After the batch write example, the result contains `Alice, 12` and `Emily, 
18`; row order is not
+guaranteed. `withFilter` enables pruning, which can leave nonmatching rows in 
candidate files.
+`executeFilter()` also evaluates the filter on individual records.
+
+## Reader resources and row reuse
+
+Close readers even if you stop before reaching the end. `forEachRemaining` 
consumes and releases
+batches and closes the reader. If you use `readBatch()` directly, call 
`releaseBatch()` for every
+returned batch and close the reader when done.
+
+Readers can reuse rows and their backing memory. Consume values before 
advancing, or copy the
+values you need to retain. See [Types and Predicates](java-types) for internal 
value representations.
+
+## Stream Read
+
+`newStreamScan()` plans successive snapshots. Startup options determine the 
initial scan;
+subsequent calls discover new changes. See [streaming 
reads](../primary-key-table/table-mode)
+and [scan configuration](../maintenance/configurations) when selecting a 
table's read mode.
+
+This example is a single-process polling loop. It prints rows and their 
`RowKind`; it is not a
+complete checkpointed pipeline.
+
+```java
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.source.ReadBuilder;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.StreamTableScan;
+import org.apache.paimon.table.source.TableRead;
+
+import java.util.List;
+
+public class StreamReadTable {
+
+    public static void main(String[] args) throws Exception {
+        try (Catalog catalog = CreateCatalog.createFilesystemCatalog()) {
+            Table table = catalog.getTable(Identifier.create("my_db", 
"my_table"));
+            ReadBuilder readBuilder = table.newReadBuilder();
+            StreamTableScan scan = readBuilder.newStreamScan();
+            // On recovery, call scan.restore(savedNextSnapshotId) before 
planning.
+            TableRead read = readBuilder.newRead();
+
+            while (!Thread.currentThread().isInterrupted()) {
+                List<Split> splits = scan.plan().splits();
+                if (!splits.isEmpty()) {
+                    try (RecordReader<InternalRow> reader = 
read.createReader(splits)) {
+                        reader.forEachRemaining(row -> System.out.println(
+                                row.getRowKind() + ": " + row.getString(0) + 
", " + row.getInt(1)));
+                    }
+                }
+
+                Long nextSnapshotId = scan.checkpoint();
+                // Persist this position together with durable downstream 
progress.
+                // Printing to stdout above is not a durable checkpoint.
+                Thread.sleep(1000);
+            }
+        }
+    }
+}
+```
+
+### Checkpoint and restore
+
+`scan.checkpoint()` returns the **next snapshot ID to plan**. It does not 
contain the planned
+splits, reader offsets, or downstream state.
+
+- In a sequential reader, finish the planned work and coordinate its durable 
downstream result
+  with the saved scan position.
+- In a distributed reader, checkpoint pending splits and reader progress as 
well as the scan
+  position. Restoring only the scan position can skip work already planned but 
not yet read.
+- Restore the saved position with `scan.restore(nextSnapshotId)` before 
planning again. Notify the
+  scan with `notifyCheckpointComplete(nextSnapshotId)` only after the 
corresponding checkpoint
+  has completed, when integrating with checkpoint/consumer tracking.
+
+Choose [snapshot retention](../maintenance/manage-snapshots) that leaves 
enough time for consumers
+to read and recover. The scan API alone does not provide end-to-end 
exactly-once processing.
+
+## Adjust Read Batch Size at Runtime
+
+Parquet and ORC readers can share a `ReadBatchSizer` to adjust the row count 
and vector
+capacity of future physical batches without recreating readers. This fragment 
uses the
+`readBuilder` and `splits` created in the batch read example:
+
+```java
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.reader.ReadBatchSizer;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.table.source.TableRead;
+
+ReadBatchSizer sizer = new ReadBatchSizer();
+TableRead read = readBuilder.newRead().withReadBatchSizer(sizer);
+try (RecordReader<InternalRow> reader = read.createReader(splits)) {
+    sizer.setBatchSize(256);
+    // Consume batches with reader.readBatch() and release each batch after 
use.
+}
+```
+
+Configure the sizer on `TableRead` before creating readers. A newly created 
sizer has no batch size,
+so readers initially use their configured default. Every explicitly set batch 
size must be positive.
+Call `clearBatchSize()` to make future physical batches use the configured 
default again.
+
+A supporting reader snapshots the batch size before starting a physical batch. 
If the size has
+changed, it replaces an idle reusable batch with vectors sized for the new 
value and then starts the
+read. The allocation is reused until the batch size changes again. 
Consequently, lowering the
+batch size reduces the vector capacity of future batches instead of only 
changing their logical
+row count.
+
+An update never mutates a batch that has already started or is still owned by 
a consumer.
+Asynchronously prefetched batches may therefore retain the previous size. For 
a pooled ORC reader,
+each idle pool entry adopts the current size the next time it is acquired, 
while in-flight entries
+keep their old vectors until released. During such a transition, old and new 
vectors can coexist.
+
+The sizer uses latest-value semantics: when updates happen faster than 
physical batches start,
+readers may skip intermediate batch sizes. Engines should avoid changing the 
size too frequently
+because each observed size change reallocates vectors and can add allocation 
and garbage-collection
+overhead. A hysteresis interval or minimum adjustment period is recommended.
+
+For concurrent scans, estimate memory using the selected batch size multiplied 
by the number of
+active and prefetched batches. This allows an engine to reduce future batch 
capacities under memory
+pressure and grow them again when more memory is available.
+
+## Next steps
+
+Use [Java Writes](java-writing) to publish data, [Types and 
Predicates](java-types) to construct
+filters, and [Local Cache](file-cache) to reduce repeated file reads.
diff --git a/docs/docs/program-api/java-types.md 
b/docs/docs/program-api/java-types.md
new file mode 100644
index 0000000000..b36c0b98e0
--- /dev/null
+++ b/docs/docs/program-api/java-types.md
@@ -0,0 +1,96 @@
+---
+title: "Types and Predicates"
+sidebar_position: 5
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Types and Predicates
+
+The Java table API exchanges `InternalRow` values. Use Paimon's internal value 
representations
+when constructing rows or predicate literals; SQL type names do not imply that 
arbitrary Java
+objects can be placed in a row.
+
+## Data Types
+
+| Paimon logical type | Internal Java value |
+| --- | --- |
+| `BOOLEAN` | `boolean` / `Boolean` |
+| `TINYINT` | `byte` / `Byte` |
+| `SMALLINT` | `short` / `Short` |
+| `INT` | `int` / `Integer` |
+| `BIGINT` | `long` / `Long` |
+| `FLOAT` | `float` / `Float` |
+| `DOUBLE` | `double` / `Double` |
+| `CHAR`, `VARCHAR`, `STRING` | `org.apache.paimon.data.BinaryString` |
+| `DECIMAL` | `org.apache.paimon.data.Decimal` |
+| `DATE` | `int`, days since the Unix epoch |
+| `TIME` | `int`, milliseconds since midnight |
+| `TIMESTAMP`, `TIMESTAMP_LTZ` | `org.apache.paimon.data.Timestamp` |
+| `BINARY`, `VARBINARY`, `BYTES` | `byte[]` |
+| `ARRAY` | `org.apache.paimon.data.InternalArray` |
+| `MAP` | `org.apache.paimon.data.InternalMap` |
+| `ROW` | `org.apache.paimon.data.InternalRow` |
+
+For example, the sample schema `(f0 STRING, f1 INT)` accepts:
+
+```java
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+
+GenericRow row = GenericRow.of(BinaryString.fromString("Alice"), 12);
+```
+
+`GenericRow` uses insert row kind by default. For changelog writes, use 
Paimon's
+`org.apache.paimon.types.RowKind` and the table's supported change semantics. 
Flink's external
+`Row` and `RowKind` are separate types; the [Flink API](flink-api) converts 
them.
+See [Data Types](../concepts/data-types) for logical type definitions and 
additional types.
+
+## Predicate Types
+
+Construct a `PredicateBuilder` from `table.rowType()`. Field indexes refer to 
the original table
+schema, even when the read uses projection.
+
+| SQL predicate | `PredicateBuilder` method |
+| --- | --- |
+| `AND`, `OR` | `and`, `or` |
+| `IS NULL`, `IS NOT NULL` | `isNull`, `isNotNull` |
+| `IN`, `NOT IN` | `in`, `notIn` |
+| `=`, `<>` | `equal`, `notEqual` |
+| `<`, `<=` | `lessThan`, `lessOrEqual` |
+| `>`, `>=` | `greaterThan`, `greaterOrEqual` |
+| `BETWEEN` | `between` |
+| `LIKE` | `like` |
+| Array membership | `arrayContains` |
+
+```java
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.predicate.PredicateBuilder;
+
+PredicateBuilder builder = new PredicateBuilder(table.rowType());
+Predicate filter = PredicateBuilder.and(
+        builder.equal(0, BinaryString.fromString("Alice")),
+        builder.greaterOrEqual(1, 12));
+```
+
+Pass predicates to `ReadBuilder.withFilter`. Enable 
`TableRead.executeFilter()` when the reader
+must also evaluate them per row; pruning alone can return candidate rows that 
do not match.
+See [Java Reads](java-reading#batch-read) for a complete example.
diff --git a/docs/docs/program-api/java-writing.md 
b/docs/docs/program-api/java-writing.md
new file mode 100644
index 0000000000..3d29235d73
--- /dev/null
+++ b/docs/docs/program-api/java-writing.md
@@ -0,0 +1,182 @@
+---
+title: "Java Writes"
+sidebar_position: 4
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Java Writes
+
+Writing records creates files; committing makes the file changes visible 
through table snapshots.
+A distributed job writes in worker tasks, gathers their `CommitMessage`s, and 
commits centrally.
+
+The examples use `CreateCatalog` and the two-column primary-key table from the
+[Java API setup](java-api). Java strings must be converted to `BinaryString` 
before writing.
+
+![Workers prepare file changes for commit ID N. A coordinator collects all 
messages and commits with the same ID; recovery retries saved 
messages.](/img/program-api-write-flow.svg)
+
+## Batch Write
+
+Create a `BatchWriteBuilder`, write records, prepare messages, and commit the 
collected messages
+once. This example upserts three rows into the sample primary-key table.
+
+```java
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.table.sink.CommitMessage;
+
+import java.util.List;
+
+public class BatchWrite {
+
+    public static void main(String[] args) throws Exception {
+        try (Catalog catalog = CreateCatalog.createFilesystemCatalog()) {
+            Table table = catalog.getTable(Identifier.create("my_db", 
"my_table"));
+            BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
+            try (BatchTableWrite write = writeBuilder.newWrite();
+                    BatchTableCommit commit = writeBuilder.newCommit()) {
+                write.write(GenericRow.of(BinaryString.fromString("Alice"), 
12));
+                write.write(GenericRow.of(BinaryString.fromString("Bob"), 5));
+                write.write(GenericRow.of(BinaryString.fromString("Emily"), 
18));
+
+                List<CommitMessage> messages = write.prepareCommit();
+                // In a distributed job, collect messages from every writer 
first.
+                commit.commit(messages);
+            }
+        }
+    }
+}
+```
+
+Closing a writer does not publish its data. Call `prepareCommit()` and commit 
the resulting
+messages before closing it. A batch committer is for one commit; use a new 
builder for another batch.
+
+### Overwrite and abort
+
+Use `table.newBatchWriteBuilder().withOverwrite()` only when you intend to 
replace existing data.
+For a partitioned table, check the overwrite scope and table options before 
selecting this mode;
+see [overwrite behavior](../flink/sql-write#overwriting-the-whole-table).
+
+`BatchTableCommit.abort(messages)` deletes files belonging to an abandoned 
write. Use it only
+for messages that are known not to have been committed and will not be 
retried. A commit exception
+alone does not prove that publishing failed; do not delete files from a 
possibly successful commit.
+
+## Stream Write
+
+A `StreamTableWrite` and `StreamTableCommit` can be reused across commits. The 
application supplies
+two identities:
+
+| Identity | Rule |
+| --- | --- |
+| `commitUser` | Shared by writers and committer of one application, stable 
across recovery, different for independent applications. |
+| `commitIdentifier` | The same value for prepare and commit, increasing for 
subsequent commits. A checkpoint ID is a common choice. |
+
+The default commit user is random. Set it explicitly, or persist and restore 
the generated value,
+when implementing recovery.
+
+```java
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.StreamTableCommit;
+import org.apache.paimon.table.sink.StreamTableWrite;
+import org.apache.paimon.table.sink.StreamWriteBuilder;
+
+import java.util.List;
+
+public class StreamWriteTable {
+
+    public static void main(String[] args) throws Exception {
+        // Supply a unique identity for a new application; retain it on 
recovery.
+        String commitUser = args[0];
+        try (Catalog catalog = CreateCatalog.createFilesystemCatalog()) {
+            Table table = catalog.getTable(Identifier.create("my_db", 
"my_table"));
+            StreamWriteBuilder writeBuilder = table.newStreamWriteBuilder()
+                    .withCommitUser(commitUser);
+            try (StreamTableWrite write = writeBuilder.newWrite();
+                    StreamTableCommit commit = writeBuilder.newCommit()) {
+                // This finite example starts a NEW application without 
recovered state.
+                for (long commitIdentifier = 0; commitIdentifier < 3; 
commitIdentifier++) {
+                    write.write(GenericRow.of(
+                            BinaryString.fromString("Alice"), 12 + (int) 
commitIdentifier));
+                    List<CommitMessage> messages =
+                            write.prepareCommit(false, commitIdentifier);
+
+                    // A recoverable pipeline saves messages and input 
progress durably here.
+                    commit.commit(commitIdentifier, messages);
+                    // The loop advances the ID only after this commit 
succeeds.
+                }
+            }
+        }
+    }
+}
+```
+
+Pass an application identity as the first argument. This finite example 
demonstrates matching
+prepare/commit IDs; it does not persist checkpoints. Do not restart it from ID 
0 with a previously
+used identity and treat it as recovery.
+
+### Recover a pending commit
+
+Save the commit user, commit IDs, prepared messages, and associated input 
progress in your
+runtime's checkpoint state. After restoring the same commit user, retry saved 
messages with
+`filterAndCommit` if the outcome of an earlier commit is uncertain:
+
+```java
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.StreamTableCommit;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+// restoredCommitIdentifier and restoredMessages come from durable checkpoint 
state.
+Map<Long, List<CommitMessage>> pending = new HashMap<>();
+pending.put(restoredCommitIdentifier, restoredMessages);
+try (StreamTableCommit commit = writeBuilder.newCommit()) {
+    commit.filterAndCommit(pending);
+}
+```
+
+`commit` does not check whether an ID was already committed. `filterAndCommit` 
filters previously
+committed IDs and publishes the remaining messages. Resume new writes with the 
next ID after
+reconciling pending commits. This protocol must be coordinated with source 
replay and writer
+recovery to provide end-to-end exactly-once behavior.
+
+## Distributed writer routing
+
+Create one logical write builder for the operation and use it for writers and 
the committer.
+For the fixed-bucket table used here, use `writeBuilder.newWriteSelector()` to 
determine which
+downstream writer should receive a record. Records for the same bucket must be 
routed consistently.
+Bucket-unaware and postpone modes return no selector. Dynamic-bucket modes do 
not support this
+selector API: they require dedicated bucket assignment and `write(row, 
bucket)` instead. See
+[data distribution](../primary-key-table/data-distribution) when selecting a 
different table layout.
+
+For a Flink job, use [FlinkSinkBuilder](flink-api#write-to-table) to integrate 
routing, checkpoints,
+and commits with the engine.
diff --git a/docs/docs/program-api/rest-api.mdx 
b/docs/docs/program-api/rest-api.mdx
index 450636b215..af6d79786f 100644
--- a/docs/docs/program-api/rest-api.mdx
+++ b/docs/docs/program-api/rest-api.mdx
@@ -1,6 +1,6 @@
 ---
-title: "REST API"
-sidebar_position: 1
+title: "REST Java Client"
+sidebar_position: 7
 ---
 
 import Stable from '@site/src/components/Stable';
@@ -25,9 +25,17 @@ specific language governing permissions and limitations
 under the License.
 -->
 
-# REST API
+# REST Java Client
 
-This is Java API for [REST](../concepts/rest/).
+`RESTApi` is a Java client for the [Paimon REST catalog](../concepts/rest/). 
Use it to issue catalog
+metadata requests without bringing in the full table read/write bundle.
+
+| What you need | API or reference |
+| --- | --- |
+| List and manage catalog objects from Java | `RESTApi`, shown below |
+| Load a `Table` and read or write rows | [Java API](java-api) with a REST 
catalog |
+| Implement an HTTP client or catalog server | [REST API 
specification](../concepts/rest/rest-api) |
+| Administrative endpoints | [Management API](../concepts/rest/management-api) 
|
 
 ## Dependency
 
@@ -53,7 +61,11 @@ Or download the jar file:
 
 </Unstable>
 
-## RESTApi
+## Connect and list tables {#restapi}
+
+Set the server URI, warehouse identifier, and authentication options. The 
warehouse value is
+interpreted by the server; it is not necessarily a filesystem path. The 
example uses the bearer
+token provider, whose configuration value is spelled `bear`.
 
 ```java
 import org.apache.paimon.options.Options;
@@ -74,14 +86,14 @@ public class RESTApiExample {
         Options options = new Options();
         options.set(URI, "<catalog server url>");
         options.set(WAREHOUSE, "my_instance_name");
-        setBearToken(options); // or setDlfToken
+        setBearerToken(options); // or setDlfToken
 
         RESTApi api = new RESTApi(options);
         List<String> tables = api.listTables("my_database");
         System.out.println(tables);
     }
 
-    private static void setBearToken(Options options) {
+    private static void setBearerToken(Options options) {
         options.set(TOKEN_PROVIDER, "bear");
         options.set(TOKEN, "<token>");
     }
@@ -94,4 +106,13 @@ public class RESTApiExample {
 }
 ```
 
-See more methods in `'RESTApi'`.
+## Authentication and table access
+
+See [bearer authentication](../concepts/rest/bear) or [DLF 
authentication](../concepts/rest/dlf)
+for provider configuration. Supply credentials through your application's 
configuration; the
+placeholders above illustrate the option names.
+
+To use the full table API, add the [Java bundle](java-api#dependency) and 
create a catalog with
+`metastore=rest`, the server `uri`, `warehouse`, and the same authentication 
options. See
+[REST catalog configuration](../concepts/rest/) for an example. Loading table 
metadata and reading
+its data files are separate operations, so configure storage access as 
required by the catalog.
diff --git a/docs/docs/program-api/rust-api.md 
b/docs/docs/program-api/rust-api.md
index c34aa46107..7a8e4b6263 100644
--- a/docs/docs/program-api/rust-api.md
+++ b/docs/docs/program-api/rust-api.md
@@ -1,9 +1,8 @@
 ---
 title: "Rust API"
-sidebar_position: 7
+sidebar_position: 9
 ---
 
-
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -25,5 +24,23 @@ under the License.
 
 # Rust API
 
-- [Paimon Rust Docs](https://paimon.apache.org/docs/rust/).
-- [Paimon Rust Repo](https://github.com/apache/paimon-rust).
+[Paimon Rust](https://github.com/apache/paimon-rust) provides native access to 
the Paimon table format
+from Rust applications. It has its own release cycle and dependency 
configuration.
+
+## Get started
+
+Follow the [Rust getting-started 
guide](https://paimon.apache.org/docs/rust/getting-started/) to add
+the crate, select storage features, create a catalog, and read a table. Use 
the crate version and
+feature flags from that guide for your chosen release.
+
+## Choose a guide
+
+| Task | Documentation |
+| --- | --- |
+| Install the crate and read a table | [Getting 
Started](https://paimon.apache.org/docs/rust/getting-started/) |
+| Explore supported features and integrations | [Rust 
Documentation](https://paimon.apache.org/docs/rust/) |
+| Build from source or contribute | [Paimon Rust 
Repository](https://github.com/apache/paimon-rust) |
+
+Catalog and table concepts are shared across implementations. See 
[Catalogs](../concepts/catalog)
+and the [Storage Specification](../concepts/spec/) for the format, then check 
the Rust documentation
+for the APIs and features supported by the version you use.
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 67670e7ec7..099fb7f0b7 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -591,12 +591,27 @@ const sidebars = {
       "id": "program-api/index"
     },
     "items": [
-      "program-api/rest-api",
+      {
+        type: "category",
+        "label": "Java API",
+        "link": {type: "doc", "id": "program-api/java-api"},
+        "items": [
+          "program-api/catalog-api",
+          "program-api/java-reading",
+          "program-api/java-writing",
+          "program-api/java-types"
+        ]
+      },
       "program-api/flink-api",
-      "program-api/java-api",
-      "program-api/catalog-api",
-      "program-api/cpp-api",
-      "program-api/rust-api",
+      {
+        type: "category",
+        "label": "Other Clients",
+        "items": [
+          "program-api/rest-api",
+          "program-api/cpp-api",
+          "program-api/rust-api"
+        ]
+      },
       "program-api/file-cache"
     ]
   },
diff --git a/docs/static/img/program-api-local-cache.svg 
b/docs/static/img/program-api-local-cache.svg
new file mode 100644
index 0000000000..6d59b627a9
--- /dev/null
+++ b/docs/static/img/program-api-local-cache.svg
@@ -0,0 +1,54 @@
+<svg xmlns="http://www.w3.org/2000/svg"; width="960" height="586" viewBox="0 0 
960 586" role="img" aria-labelledby="title desc">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<title id="title">Local blocks reduce repeated storage reads</title>
+<desc id="desc">Eligible immutable file reads check the local memory or disk 
cache. Hits return a cached block. Misses fetch and cache a block from storage. 
Non-whitelisted files and mutable hints bypass the cache.</desc>
+<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" 
markerHeight="7" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" 
fill="#526277"/></marker></defs>
+<g font-family="Arial, Helvetica, sans-serif">
+<rect x="1" y="1" width="958" height="584" rx="12" fill="#ffffff" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="28" y="44" font-size="27" fill="#172b4d" font-weight="700" 
text-anchor="start">Local blocks reduce repeated storage reads</text>
+<text x="28" y="76" font-size="18" fill="#526277" font-weight="400" 
text-anchor="start">Enable caching on the catalog and choose which file types 
to cache.</text>
+<rect x="28" y="116" width="238" height="104" rx="8" fill="#eaf2ff" 
stroke="#2463b4" stroke-width="1.5"/>
+<text x="147.0" y="149" font-size="21" fill="#2463b4" font-weight="700" 
text-anchor="middle">File read</text>
+<text x="147.0" y="177" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Check file type</text>
+<text x="147.0" y="202" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">and mutable hints</text>
+<rect x="365" y="116" width="257" height="104" rx="8" fill="#e8f7f2" 
stroke="#087f6e" stroke-width="1.5"/>
+<text x="493.5" y="149" font-size="21" fill="#087f6e" font-weight="700" 
text-anchor="middle">Local block cache</text>
+<text x="493.5" y="177" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Memory or disk</text>
+<text x="493.5" y="202" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Bounded by max-size</text>
+<rect x="720" y="116" width="212" height="104" rx="8" fill="#f5f7fb" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="826.0" y="149" font-size="21" fill="#172b4d" font-weight="700" 
text-anchor="middle">Storage</text>
+<text x="826.0" y="177" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">S3 / OSS / HDFS</text>
+<text x="826.0" y="202" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">or filesystem</text>
+<path d="M 266 165 H 365" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<text x="315" y="142" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Eligible</text>
+<path d="M 622 165 H 720" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<text x="671" y="142" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Miss</text>
+<path d="M 824 220 V 265 H 560 V 220" fill="none" stroke="#526277" 
stroke-width="2" marker-end="url(#arrow)"/>
+<text x="744" y="292" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Fetch block and cache it</text>
+<text x="28" y="332" font-size="17" fill="#526277" font-weight="400" 
text-anchor="start">Other file types, LATEST and</text>
+<text x="28" y="357" font-size="17" fill="#526277" font-weight="400" 
text-anchor="start">EARLIEST read storage directly.</text>
+<rect x="365" y="390" width="257" height="80" rx="8" fill="#e8f7f2" 
stroke="#087f6e" stroke-width="1.5"/>
+<text x="493.5" y="423" font-size="21" fill="#087f6e" font-weight="700" 
text-anchor="middle">Return bytes</text>
+<text x="493.5" y="451" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Hit: reuse cached block</text>
+<path d="M 493 220 V 390" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<text x="28" y="517" font-size="18" fill="#172b4d" font-weight="700" 
text-anchor="start">Default whitelist: meta,global-index. Add data explicitly 
to cache data files.</text>
+<text x="28" y="548" font-size="17" fill="#526277" font-weight="400" 
text-anchor="start">Block hits avoid content downloads; file-status and other 
metadata calls can still occur.</text>
+</g>
+</svg>
diff --git a/docs/static/img/program-api-overview.svg 
b/docs/static/img/program-api-overview.svg
new file mode 100644
index 0000000000..b017468323
--- /dev/null
+++ b/docs/static/img/program-api-overview.svg
@@ -0,0 +1,63 @@
+<svg xmlns="http://www.w3.org/2000/svg"; width="960" height="560" viewBox="0 0 
960 560" role="img" aria-labelledby="title desc">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<title id="title">Choose the layer your application needs</title>
+<desc id="desc">Catalog clients manage metadata. Table APIs plan reads and 
prepare writes. Flink and Spark integrate table access with execution and 
recovery.</desc>
+<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" 
markerHeight="7" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" 
fill="#526277"/></marker></defs>
+<g font-family="Arial, Helvetica, sans-serif">
+<rect x="1" y="1" width="958" height="558" rx="12" fill="#ffffff" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="28" y="44" font-size="27" fill="#172b4d" font-weight="700" 
text-anchor="start">Choose the layer your application needs</text>
+<text x="28" y="76" font-size="18" fill="#526277" font-weight="400" 
text-anchor="start">Catalogs resolve tables; table APIs expose data access; 
engines coordinate execution.</text>
+<rect x="28" y="108" width="275" height="104" rx="8" fill="#eaf2ff" 
stroke="#2463b4" stroke-width="1.5"/>
+<text x="165.5" y="141" font-size="21" fill="#2463b4" font-weight="700" 
text-anchor="middle">Catalog clients</text>
+<text x="165.5" y="169" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Java Catalog / RESTApi</text>
+<text x="165.5" y="194" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">C++ / Rust catalogs</text>
+<rect x="343" y="108" width="275" height="104" rx="8" fill="#eaf2ff" 
stroke="#2463b4" stroke-width="1.5"/>
+<text x="480.5" y="141" font-size="21" fill="#2463b4" font-weight="700" 
text-anchor="middle">Table APIs</text>
+<text x="480.5" y="169" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Java / C++ / Rust / Python</text>
+<text x="480.5" y="194" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Rows, splits, commit messages</text>
+<rect x="658" y="108" width="274" height="104" rx="8" fill="#e8f7f2" 
stroke="#087f6e" stroke-width="1.5"/>
+<text x="795.0" y="141" font-size="21" fill="#087f6e" font-weight="700" 
text-anchor="middle">Engine integrations</text>
+<text x="795.0" y="169" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Flink / Spark</text>
+<text x="795.0" y="194" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Jobs and checkpoints</text>
+<path d="M 303 160 H 343" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<path d="M 658 160 H 618" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<rect x="28" y="262" width="275" height="109" rx="8" fill="#f5f7fb" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="165.5" y="295" font-size="21" fill="#172b4d" font-weight="700" 
text-anchor="middle">Catalog metadata</text>
+<text x="165.5" y="323" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Databases and table schemas</text>
+<text x="165.5" y="348" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Names, locations, options</text>
+<rect x="343" y="262" width="275" height="109" rx="8" fill="#f5f7fb" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="480.5" y="295" font-size="21" fill="#172b4d" font-weight="700" 
text-anchor="middle">Read and write builders</text>
+<text x="480.5" y="323" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Plan splits → read rows</text>
+<text x="480.5" y="348" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Write files → prepare changes</text>
+<rect x="658" y="262" width="274" height="109" rx="8" fill="#f5f7fb" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="795.0" y="295" font-size="21" fill="#172b4d" font-weight="700" 
text-anchor="middle">Runtime coordination</text>
+<text x="795.0" y="323" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Assign splits and route rows</text>
+<text x="795.0" y="348" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Commit and recover</text>
+<path d="M 165 212 V 262" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<path d="M 480 212 V 262" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<path d="M 795 212 V 262" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<rect x="28" y="422" width="904" height="99" rx="8" fill="#e8f7f2" 
stroke="#087f6e" stroke-width="1.5"/>
+<text x="480" y="458" font-size="23" fill="#087f6e" font-weight="700" 
text-anchor="middle">Paimon table storage</text>
+<text x="480" y="488" font-size="18" fill="#526277" font-weight="400" 
text-anchor="middle">Schemas • snapshots • manifests • data files • 
indexes</text>
+<path d="M 165 371 V 422" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<path d="M 480 371 V 422" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<path d="M 795 371 V 422" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+</g>
+</svg>
diff --git a/docs/static/img/program-api-read-flow.svg 
b/docs/static/img/program-api-read-flow.svg
new file mode 100644
index 0000000000..47c63f4906
--- /dev/null
+++ b/docs/static/img/program-api-read-flow.svg
@@ -0,0 +1,52 @@
+<svg xmlns="http://www.w3.org/2000/svg"; width="960" height="534" viewBox="0 0 
960 534" role="img" aria-labelledby="title desc">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<title id="title">Plan once, read the assigned splits</title>
+<desc id="desc">A coordinator configures a ReadBuilder and plans splits. 
Worker readers use the same read configuration. A streaming checkpoint must 
also account for pending splits and reader progress.</desc>
+<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" 
markerHeight="7" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" 
fill="#526277"/></marker></defs>
+<g font-family="Arial, Helvetica, sans-serif">
+<rect x="1" y="1" width="958" height="532" rx="12" fill="#ffffff" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="28" y="44" font-size="27" fill="#172b4d" font-weight="700" 
text-anchor="start">Plan once, read the assigned splits</text>
+<text x="28" y="76" font-size="18" fill="#526277" font-weight="400" 
text-anchor="start">Use the same filter and projection when planning and 
reading.</text>
+<rect x="28" y="110" width="282" height="118" rx="8" fill="#eaf2ff" 
stroke="#2463b4" stroke-width="1.5"/>
+<text x="169.0" y="143" font-size="21" fill="#2463b4" font-weight="700" 
text-anchor="middle">Coordinator</text>
+<text x="169.0" y="171" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">ReadBuilder</text>
+<text x="169.0" y="196" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">scan.plan().splits()</text>
+<rect x="385" y="110" width="200" height="118" rx="8" fill="#f5f7fb" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="485.0" y="143" font-size="21" fill="#172b4d" font-weight="700" 
text-anchor="middle">Splits</text>
+<text x="485.0" y="171" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Split A</text>
+<text x="485.0" y="196" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Split B</text>
+<rect x="660" y="110" width="272" height="93" rx="8" fill="#e8f7f2" 
stroke="#087f6e" stroke-width="1.5"/>
+<text x="796.0" y="143" font-size="21" fill="#087f6e" font-weight="700" 
text-anchor="middle">Reader task 1</text>
+<text x="796.0" y="171" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Read Split A</text>
+<rect x="660" y="235" width="272" height="93" rx="8" fill="#e8f7f2" 
stroke="#087f6e" stroke-width="1.5"/>
+<text x="796.0" y="268" font-size="21" fill="#087f6e" font-weight="700" 
text-anchor="middle">Reader task 2</text>
+<text x="796.0" y="296" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Read Split B</text>
+<path d="M 310 167 H 385" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<path d="M 585 147 H 660" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<path d="M 585 192 H 622 V 278 H 660" fill="none" stroke="#526277" 
stroke-width="2" marker-end="url(#arrow)"/>
+<text x="28" y="273" font-size="20" fill="#172b4d" font-weight="700" 
text-anchor="start">Each reader</text>
+<text x="28" y="305" font-size="18" fill="#526277" font-weight="400" 
text-anchor="start">newRead() → createReader(assigned splits)</text>
+<text x="28" y="336" font-size="18" fill="#526277" font-weight="400" 
text-anchor="start">executeFilter() adds per-row filtering.</text>
+<text x="28" y="367" font-size="18" fill="#526277" font-weight="400" 
text-anchor="start">Release batches and close readers after use.</text>
+<rect x="28" y="408" width="904" height="95" rx="8" fill="#fff6e8" 
stroke="#c5862b" stroke-width="1.5"/>
+<text x="48" y="440" font-size="21" fill="#172b4d" font-weight="700" 
text-anchor="start">Streaming recovery needs more than the scan position</text>
+<text x="48" y="470" font-size="18" fill="#526277" font-weight="400" 
text-anchor="start">Save next snapshot ID + pending splits + reader and 
downstream progress.</text>
+</g>
+</svg>
diff --git a/docs/static/img/program-api-write-flow.svg 
b/docs/static/img/program-api-write-flow.svg
new file mode 100644
index 0000000000..e31bea4cf3
--- /dev/null
+++ b/docs/static/img/program-api-write-flow.svg
@@ -0,0 +1,56 @@
+<svg xmlns="http://www.w3.org/2000/svg"; width="960" height="598" viewBox="0 0 
960 598" role="img" aria-labelledby="title desc">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<title id="title">Prepare files, then publish a commit</title>
+<desc id="desc">For streaming commit N, workers write and prepare messages 
with N. A coordinator commits all messages with N and the same commit user. A 
recovery path restores saved messages and filters already committed IDs.</desc>
+<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" 
markerHeight="7" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" 
fill="#526277"/></marker></defs>
+<g font-family="Arial, Helvetica, sans-serif">
+<rect x="1" y="1" width="958" height="596" rx="12" fill="#ffffff" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="28" y="44" font-size="27" fill="#172b4d" font-weight="700" 
text-anchor="start">Prepare files, then publish a commit</text>
+<text x="28" y="76" font-size="18" fill="#526277" font-weight="400" 
text-anchor="start">Streaming example: one application identity, one matching 
ID for each commit.</text>
+<rect x="28" y="111" width="277" height="110" rx="8" fill="#eaf2ff" 
stroke="#2463b4" stroke-width="1.5"/>
+<text x="166.5" y="144" font-size="21" fill="#2463b4" font-weight="700" 
text-anchor="middle">Worker writers</text>
+<text x="166.5" y="172" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">write(row)</text>
+<text x="166.5" y="197" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">prepareCommit(false, N)</text>
+<rect x="350" y="111" width="242" height="110" rx="8" fill="#f5f7fb" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="471.0" y="144" font-size="21" fill="#172b4d" font-weight="700" 
text-anchor="middle">Prepared messages</text>
+<text x="471.0" y="172" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Collect from all writers</text>
+<text x="471.0" y="197" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Keep commit ID N</text>
+<rect x="637" y="111" width="295" height="110" rx="8" fill="#e8f7f2" 
stroke="#087f6e" stroke-width="1.5"/>
+<text x="784.5" y="144" font-size="21" fill="#087f6e" font-weight="700" 
text-anchor="middle">Coordinator</text>
+<text x="784.5" y="172" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">commit(N, messages)</text>
+<text x="784.5" y="197" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Publish snapshot changes</text>
+<path d="M 305 166 H 350" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<path d="M 592 166 H 637" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<rect x="28" y="270" width="904" height="64" rx="8" fill="#eaf2ff" 
stroke="#2463b4" stroke-width="1.5"/>
+<text x="480" y="308" font-size="19" fill="#2463b4" font-weight="700" 
text-anchor="middle">Use the same commitUser and commitIdentifier for prepare 
and commit.</text>
+<path d="M 471 221 V 270" fill="none" stroke="#526277" stroke-width="2" 
stroke-dasharray="6 5" marker-end="url(#arrow)"/>
+<rect x="28" y="386" width="418" height="111" rx="8" fill="#f5f7fb" 
stroke="#d7dfeb" stroke-width="1.5"/>
+<text x="237.0" y="419" font-size="21" fill="#172b4d" font-weight="700" 
text-anchor="middle">Recover an uncertain commit</text>
+<text x="237.0" y="447" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Restore saved user, IDs, messages</text>
+<text x="237.0" y="472" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">and associated input progress</text>
+<rect x="514" y="386" width="418" height="111" rx="8" fill="#e8f7f2" 
stroke="#087f6e" stroke-width="1.5"/>
+<text x="723.0" y="419" font-size="21" fill="#087f6e" font-weight="700" 
text-anchor="middle">Retry saved messages</text>
+<text x="723.0" y="447" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">filterAndCommit(pending)</text>
+<text x="723.0" y="472" font-size="17" fill="#526277" font-weight="400" 
text-anchor="middle">Skip IDs already committed</text>
+<path d="M 446 441 H 514" fill="none" stroke="#526277" stroke-width="2" 
marker-end="url(#arrow)"/>
+<text x="28" y="545" font-size="19" fill="#172b4d" font-weight="700" 
text-anchor="start">Advance the ID for new work after reconciling pending 
commits.</text>
+<text x="28" y="574" font-size="17" fill="#526277" font-weight="400" 
text-anchor="start">Batch writes follow prepare → commit once, without an 
application-supplied commit ID.</text>
+</g>
+</svg>

Reply via email to