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 eb797e1111 [core] Support object table in Jdbc and Hive catalog (#8624)
eb797e1111 is described below
commit eb797e11119be30b56d975c1c3618c707eb3e7c3
Author: eye-gu <[email protected]>
AuthorDate: Thu Jul 16 13:47:39 2026 +0800
[core] Support object table in Jdbc and Hive catalog (#8624)
---
.../org/apache/paimon/catalog/AbstractCatalog.java | 54 ++++++++++++++--
.../java/org/apache/paimon/jdbc/JdbcCatalog.java | 75 ++++++++++++++++++++--
.../org/apache/paimon/jdbc/JdbcCatalogTest.java | 66 +++++++++++++++++++
.../java/org/apache/paimon/hive/HiveCatalog.java | 38 +++++++++++
.../org/apache/paimon/hive/HiveCatalogTest.java | 65 +++++++++++++++++++
5 files changed, 288 insertions(+), 10 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
index edcdfa8eec..6c97bb9808 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
@@ -43,8 +43,11 @@ import org.apache.paimon.table.FormatTable;
import org.apache.paimon.table.Instant;
import org.apache.paimon.table.Table;
import org.apache.paimon.table.TableSnapshot;
+import org.apache.paimon.table.object.ObjectTable;
import org.apache.paimon.table.sink.TableCommitImpl;
import org.apache.paimon.table.system.SystemTableLoader;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.SnapshotNotExistException;
import org.slf4j.Logger;
@@ -449,10 +452,8 @@ public abstract class AbstractCatalog implements Catalog {
createFormatTable(identifier, schema);
break;
case OBJECT_TABLE:
- throw new UnsupportedOperationException(
- String.format(
- "Catalog %s cannot support object tables.",
- this.getClass().getName()));
+ createObjectTable(identifier, schema);
+ break;
}
}
@@ -766,6 +767,38 @@ public abstract class AbstractCatalog implements Catalog {
this.getClass().getName() + " currently does not support
format table");
}
+ /**
+ * Create an {@link ObjectTable} identified by the given {@link
Identifier}.
+ *
+ * @param identifier Path of the table
+ * @param schema Schema of the table
+ */
+ public void createObjectTable(Identifier identifier, Schema schema) {
+ throw new UnsupportedOperationException(
+ this.getClass().getName() + " currently does not support
object table");
+ }
+
+ /**
+ * Build a {@link Schema} with the fixed fields of {@link ObjectTable} and
the options from the
+ * given schema. Object Table has a fixed schema and does not support
custom fields, so the
+ * user-provided fields are ignored.
+ *
+ * @param schema the user-provided schema containing options
(type=object-table, path, etc.)
+ * @return a new schema with ObjectTable's fixed fields and the
user-provided options
+ */
+ protected static Schema buildObjectTableSchema(Schema schema) {
+ RowType schemaRowType = ObjectTable.SCHEMA;
+ Schema.Builder builder = Schema.newBuilder();
+ for (DataField field : schemaRowType.getFields()) {
+ builder.column(field.name(), field.type(), field.description());
+ }
+ builder.options(schema.options());
+ if (schema.comment() != null) {
+ builder.comment(schema.comment());
+ }
+ return builder.build();
+ }
+
/**
* Get warehouse path for specified database. If a catalog would like to
provide individual path
* for each database, this method can be `Override` in that catalog.
@@ -813,7 +846,11 @@ public abstract class AbstractCatalog implements Catalog {
}
private void validateCustomTablePath(Map<String, String> options) {
- if (!allowCustomTablePath() &&
options.containsKey(CoreOptions.PATH.key())) {
+ TableType tableType = Options.fromMap(options).get(TYPE);
+ boolean isObjectTable = tableType == TableType.OBJECT_TABLE;
+ if (!isObjectTable
+ && !allowCustomTablePath()
+ && options.containsKey(CoreOptions.PATH.key())) {
throw new UnsupportedOperationException(
String.format(
"The current catalog %s does not support
specifying the table path when creating a table.",
@@ -870,7 +907,12 @@ public abstract class AbstractCatalog implements Catalog {
return s.copy(branchOptions.toMap());
});
}
- schema.ifPresent(s -> s.options().put(PATH.key(),
tablePath.toString()));
+ schema.ifPresent(s -> putPathOption(s, tablePath));
return schema;
}
+
+ /** Set the PATH option on the loaded schema. */
+ protected void putPathOption(TableSchema schema, Path tablePath) {
+ schema.options().put(PATH.key(), tablePath.toString());
+ }
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcCatalog.java
b/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcCatalog.java
index e489297462..79687c9368 100644
--- a/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/jdbc/JdbcCatalog.java
@@ -420,10 +420,65 @@ public class JdbcCatalog extends AbstractCatalog {
}
break;
case OBJECT_TABLE:
- throw new UnsupportedOperationException(
+ try {
+ runWithLock(
+ identifier,
+ () -> {
+ if (!validateTableNotExists(identifier,
ignoreIfExists)) {
+ return null;
+ }
+ createObjectTable(identifier, schema);
+ return null;
+ });
+ } catch (TableAlreadyExistException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to create table " +
identifier.getFullName(), e);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void createObjectTable(Identifier identifier, Schema schema) {
+ try {
+ Path tablePath = getTableLocation(identifier);
+ schema.options().putIfAbsent(PATH.key(), tablePath.toString());
+ Schema objectSchema = buildObjectTableSchema(schema);
+ fileIO.mkdirs(tablePath);
+
+ // Write schema file via SchemaManager.commit()
+ SchemaManager schemaManager = getSchemaManager(identifier);
+ TableSchema tableSchema = TableSchema.create(0, objectSchema);
+ if (!schemaManager.commit(tableSchema)) {
+ throw new RuntimeException(
+ "Failed to commit schema for object table " +
identifier);
+ }
+
+ // Register table in JDBC catalog
+ if (!JdbcUtils.insertTable(
+ connections,
+ catalogKey,
+ identifier.getDatabaseName(),
+ identifier.getTableName())) {
+ fileIO.deleteDirectoryQuietly(tablePath);
+ throw new RuntimeException(
String.format(
- "Catalog %s cannot support object tables.",
- this.getClass().getName()));
+ "Failed to create table %s in catalog %s",
+ identifier.getFullName(), catalogKey));
+ }
+ if (syncTableProperties()) {
+ JdbcUtils.insertTableProperties(
+ connections,
+ catalogKey,
+ identifier.getDatabaseName(),
+ identifier.getTableName(),
+ collectTableProperties(tableSchema));
+ }
+ LOG.debug("Successfully created object table: {}", identifier);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create table " +
identifier.getFullName(), e);
}
}
@@ -665,6 +720,16 @@ public class JdbcCatalog extends AbstractCatalog {
}
}
+ /**
+ * Use {@code putIfAbsent} because a JDBC catalog stores the schema file
at the warehouse
+ * default path while an object table's PATH option may point to a
different user-specified data
+ * directory. Overwriting would wrongly redirect the object table to the
schema directory.
+ */
+ @Override
+ protected void putPathOption(TableSchema schema, Path tablePath) {
+ schema.options().putIfAbsent(PATH.key(), tablePath.toString());
+ }
+
@Override
protected TableSchema loadTableSchema(Identifier identifier) throws
TableNotExistException {
assertMainBranch(identifier);
@@ -876,7 +941,9 @@ public class JdbcCatalog extends AbstractCatalog {
}
private void validateCustomTablePath(Map<String, String> tableOptions) {
- if (!allowCustomTablePath() && tableOptions.containsKey(PATH.key())) {
+ TableType tableType = Options.fromMap(tableOptions).get(TYPE);
+ boolean isObjectTable = tableType == TableType.OBJECT_TABLE;
+ if (!isObjectTable && !allowCustomTablePath() &&
tableOptions.containsKey(PATH.key())) {
throw new UnsupportedOperationException(
String.format(
"The current catalog %s does not support
specifying the table path when creating a table.",
diff --git
a/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java
b/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java
index 3e6209220a..7f985f938e 100644
--- a/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java
@@ -18,6 +18,8 @@
package org.apache.paimon.jdbc;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.TableType;
import org.apache.paimon.catalog.Catalog;
import org.apache.paimon.catalog.CatalogContext;
import org.apache.paimon.catalog.CatalogTestBase;
@@ -28,6 +30,7 @@ import org.apache.paimon.options.Options;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaChange;
import org.apache.paimon.table.Table;
+import org.apache.paimon.table.object.ObjectTable;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.view.View;
@@ -103,6 +106,69 @@ public class JdbcCatalogTest extends CatalogTestBase {
@Test
public void testGetTable() throws Exception {}
+ @Test
+ public void testObjectTable() throws Exception {
+ String databaseName = "object_table_db";
+ String tableName = "object_table";
+ catalog.createDatabase(databaseName, false);
+ Identifier identifier = Identifier.create(databaseName, tableName);
+
+ // Create object table with only options (type=object-table)
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.TYPE.key(), TableType.OBJECT_TABLE.toString());
+ Schema schema = Schema.newBuilder().options(options).build();
+
+ catalog.createTable(identifier, schema, false);
+
+ // Verify table exists in JDBC catalog
+ assertThat(catalog.listTables(databaseName)).contains(tableName);
+
+ // Verify getTable returns ObjectTable instance
+ Table table = catalog.getTable(identifier);
+ assertThat(table).isInstanceOf(ObjectTable.class);
+
+ ObjectTable objectTable = (ObjectTable) table;
+ // Verify fixed schema fields
+ assertThat(objectTable.rowType().getFieldNames())
+ .containsExactly("path", "name", "length", "mtime", "atime",
"owner");
+ // Verify location is set (defaults to table path)
+ assertThat(objectTable.location()).isNotNull();
+ // Verify options contain type=object-table
+ assertThat(objectTable.options().get(CoreOptions.TYPE.key()))
+ .isEqualTo(TableType.OBJECT_TABLE.toString());
+
+ // Drop table and verify it's gone
+ catalog.dropTable(identifier, false);
+ assertThat(catalog.listTables(databaseName)).doesNotContain(tableName);
+ }
+
+ @Test
+ public void testObjectTableWithCustomPath() throws Exception {
+ String databaseName = "object_table_custom_db";
+ String tableName = "object_table_custom";
+ catalog.createDatabase(databaseName, false);
+ Identifier identifier = Identifier.create(databaseName, tableName);
+
+ // Create object table with custom path
+ String customPath =
+ new Path(warehouse, databaseName + ".db/" + tableName +
"_data").toString();
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.TYPE.key(), TableType.OBJECT_TABLE.toString());
+ options.put(CoreOptions.PATH.key(), customPath);
+ Schema schema = Schema.newBuilder().options(options).build();
+
+ catalog.createTable(identifier, schema, false);
+
+ // Verify getTable returns ObjectTable with the custom location
+ Table table = catalog.getTable(identifier);
+ assertThat(table).isInstanceOf(ObjectTable.class);
+ ObjectTable objectTable = (ObjectTable) table;
+ assertThat(objectTable.location()).isEqualTo(customPath);
+
+ // Clean up
+ catalog.dropTable(identifier, false);
+ }
+
@Test
public void testDropTableWhenTablePathMissing() throws Exception {
String databaseName = "test_db";
diff --git
a/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java
b/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java
index d21b961098..926fc024f2 100644
---
a/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java
+++
b/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java
@@ -105,6 +105,7 @@ import static
org.apache.hadoop.hive.serde.serdeConstants.FIELD_DELIM;
import static org.apache.paimon.CoreOptions.DATA_FILE_PATH_DIRECTORY;
import static org.apache.paimon.CoreOptions.FILE_FORMAT;
import static org.apache.paimon.CoreOptions.PARTITION_EXPIRATION_TIME;
+import static org.apache.paimon.CoreOptions.PATH;
import static org.apache.paimon.CoreOptions.TYPE;
import static org.apache.paimon.TableType.FORMAT_TABLE;
import static org.apache.paimon.catalog.CatalogUtils.checkNotBranch;
@@ -1044,6 +1045,43 @@ public class HiveCatalog extends AbstractCatalog {
}
}
+ @Override
+ public void createObjectTable(Identifier identifier, Schema schema) {
+ Pair<Path, Boolean> pair = initialTableLocation(schema.options(),
identifier);
+ Path location = pair.getLeft();
+ boolean externalTable = pair.getRight();
+ schema.options().putIfAbsent(PATH.key(), location.toString());
+ Schema objectSchema = buildObjectTableSchema(schema);
+ TableSchema newSchema = TableSchema.create(0, objectSchema);
+
+ try {
+ // Create schema directory and write schema file via
SchemaManager.commit()
+ FileIO tableFileIO = fileIO(location);
+ tableFileIO.mkdirs(location);
+ boolean committed =
+ runWithLock(
+ identifier,
+ () -> schemaManager(identifier,
location).commit(newSchema));
+ if (!committed) {
+ throw new RuntimeException(
+ "Failed to commit schema for object table " +
identifier);
+ }
+
+ // Create HMS table
+ Table hiveTable = createHiveTable(identifier, newSchema, location,
externalTable);
+ clients().execute(client -> client.createTable(hiveTable));
+ } catch (Exception e) {
+ if (!externalTable) {
+ try {
+ fileIO(location).deleteDirectoryQuietly(location);
+ } catch (Exception ee) {
+ LOG.error("Delete directory[{}] fail for table {}",
location, identifier, ee);
+ }
+ }
+ throw new RuntimeException("Failed to create table " +
identifier.getFullName(), e);
+ }
+ }
+
private boolean usingExternalTable(Map<String, String> tableOptions) {
CatalogTableType tableType =
OptionsUtils.convertToEnum(
diff --git
a/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java
b/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java
index a6f7dd7d1c..e1053c4715 100644
---
a/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java
+++
b/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java
@@ -19,6 +19,7 @@
package org.apache.paimon.hive;
import org.apache.paimon.CoreOptions;
+import org.apache.paimon.TableType;
import org.apache.paimon.catalog.Catalog;
import org.apache.paimon.catalog.CatalogContext;
import org.apache.paimon.catalog.CatalogTestBase;
@@ -31,6 +32,7 @@ import org.apache.paimon.partition.Partition;
import org.apache.paimon.partition.PartitionStatistics;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaChange;
+import org.apache.paimon.table.object.ObjectTable;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.utils.CommonTestUtils;
@@ -456,6 +458,69 @@ public class HiveCatalogTest extends CatalogTestBase {
assertThat(actual.lastFileCreationTime()).isEqualTo(expected.lastFileCreationTime()
/ 1000);
}
+ @Test
+ public void testObjectTable() throws Exception {
+ String databaseName = "object_table_db";
+ String tableName = "object_table";
+ catalog.createDatabase(databaseName, false);
+ Identifier identifier = Identifier.create(databaseName, tableName);
+
+ // Create object table with only options (type=object-table)
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.TYPE.key(), TableType.OBJECT_TABLE.toString());
+ Schema schema = Schema.newBuilder().options(options).build();
+
+ catalog.createTable(identifier, schema, false);
+
+ // Verify table exists in HMS
+ assertThat(catalog.listTables(databaseName)).contains(tableName);
+
+ // Verify getTable returns ObjectTable instance
+ org.apache.paimon.table.Table table = catalog.getTable(identifier);
+ assertThat(table).isInstanceOf(ObjectTable.class);
+
+ ObjectTable objectTable = (ObjectTable) table;
+ // Verify fixed schema fields
+ assertThat(objectTable.rowType().getFieldNames())
+ .containsExactly("path", "name", "length", "mtime", "atime",
"owner");
+ // Verify location is set (defaults to table path)
+ assertThat(objectTable.location()).isNotNull();
+ // Verify options contain type=object-table
+ assertThat(objectTable.options().get(CoreOptions.TYPE.key()))
+ .isEqualTo(TableType.OBJECT_TABLE.toString());
+
+ // Drop table and verify it's gone
+ catalog.dropTable(identifier, false);
+ assertThat(catalog.listTables(databaseName)).doesNotContain(tableName);
+ }
+
+ @Test
+ public void testObjectTableWithCustomPath() throws Exception {
+ String databaseName = "object_table_custom_db";
+ String tableName = "object_table_custom";
+ catalog.createDatabase(databaseName, false);
+ Identifier identifier = Identifier.create(databaseName, tableName);
+
+ // Create object table with custom path
+ String customPath =
+ new Path(warehouse, databaseName + ".db/" + tableName +
"_data").toString();
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.TYPE.key(), TableType.OBJECT_TABLE.toString());
+ options.put(CoreOptions.PATH.key(), customPath);
+ Schema schema = Schema.newBuilder().options(options).build();
+
+ catalog.createTable(identifier, schema, false);
+
+ // Verify getTable returns ObjectTable with the custom location
+ org.apache.paimon.table.Table table = catalog.getTable(identifier);
+ assertThat(table).isInstanceOf(ObjectTable.class);
+ ObjectTable objectTable = (ObjectTable) table;
+ assertThat(objectTable.location()).isEqualTo(customPath);
+
+ // Clean up
+ catalog.dropTable(identifier, false);
+ }
+
@Test
public void testCreateExternalTableWithLocation(@TempDir
java.nio.file.Path tempDir)
throws Exception {