hudi-agent commented on code in PR #20012:
URL: https://github.com/apache/hudi/pull/20012#discussion_r4078413965


##########
hudi-trino/src/main/java/io/trino/plugin/hudi/HudiTableProperties.java:
##########
@@ -55,6 +93,99 @@ public HudiTableProperties()
                                 .map(name -> name.toLowerCase(ENGLISH))
                                 .collect(toImmutableList()),
                         value -> value))
+                .add(enumProperty(
+                        TABLE_TYPE_PROPERTY,
+                        "Hudi table type",
+                        HoodieTableType.class,

Review Comment:
   🤖 nit: `partitioned_by`, `primary_key` and `ordering_fields` now repeat the 
same 10-line lower-casing `ArrayType(VARCHAR)` PropertyMetadata block; it might 
be worth a small `lowerCasedStringListProperty(name, description)` helper so 
the three stay in sync.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-trino/src/main/java/io/trino/plugin/hudi/HudiMetadata.java:
##########
@@ -304,6 +321,202 @@ public Optional<Object> getInfo(ConnectorSession session, 
ConnectorTableHandle t
         return Optional.of(new HudiTableInfo(table.getSchemaTableName(), 
table.getTableType().name(), table.getBasePath()));
     }
 
+    /**
+     * Creates an empty table: its {@code .hoodie} directory on storage and 
its catalog entry.
+     * <p>
+     * This is the single-call path only. {@code beginCreateTable}/{@code 
finishCreateTable} and a
+     * page sink are deliberately absent, so {@code CREATE TABLE AS} and 
{@code INSERT} still fail as
+     * unsupported; no rows are written here.
+     * <p>
+     * An explicit {@code location} makes the table external, matching Hudi's 
Spark SQL behaviour. An
+     * omitted one makes it managed, at {@code <schemaLocation>/<tableName>}, 
which is what decides
+     * whether {@code DROP TABLE} later deletes the data.
+     */
+    @Override
+    public void createTable(ConnectorSession session, ConnectorTableMetadata 
tableMetadata, SaveMode saveMode)
+    {
+        SchemaTableName schemaTableName = tableMetadata.getTable();
+        if (saveMode == SaveMode.REPLACE) {
+            // Replacing a table means deciding what happens to the rows 
already in it, which is the
+            // write path this connector does not yet have.
+            throw new TrinoException(NOT_SUPPORTED, "This connector does not 
support replacing tables");
+        }
+        Database database = 
metastore.getDatabase(schemaTableName.getSchemaName())
+                .orElseThrow(() -> new 
SchemaNotFoundException(schemaTableName.getSchemaName()));
+        if (metastore.getTable(schemaTableName.getSchemaName(), 
schemaTableName.getTableName()).isPresent()) {
+            if (saveMode == SaveMode.IGNORE) {
+                return;
+            }
+            throw new TrinoException(ALREADY_EXISTS, "Table already exists: " 
+ schemaTableName);
+        }
+
+        // Everything that can be rejected is rejected before storage is 
touched, so a bad statement
+        // leaves nothing behind.
+        HudiTableValidation.validateCreateTable(tableMetadata);
+
+        Map<String, Object> properties = tableMetadata.getProperties();
+        Optional<String> explicitLocation = getTableLocation(properties);
+        boolean external = explicitLocation.isPresent();
+        String basePath = explicitLocation.orElseGet(() -> 
defaultTableLocation(database, schemaTableName));
+
+        TrinoFileSystem fileSystem = fileSystemFactory.create(session);
+        checkLocationIsEmpty(fileSystem, basePath);
+
+        // One schema object produces both hoodie.table.create.schema and the 
metastore column list,
+        // so the two cannot disagree (HUDI-9435).
+        HoodieSchema tableSchema = HudiSchemaConverter.toTableSchema(
+                tableMetadata.getColumns(), schemaTableName.getTableName());
+        Table table = HudiMetastoreTables.buildTable(
+                schemaTableName,
+                basePath,
+                getTableType(properties),
+                tableSchema,
+                getPartitionedBy(properties),
+                external,
+                Optional.of(session.getUser()),
+                tableMetadata.getComment());
+        try {
+            HudiTableInitializer.initializeTable(fileSystem, basePath, 
tableMetadata, tableSchema);
+            metastore.createTable(table, NO_PRIVILEGES);
+        }
+        catch (TableAlreadyExistsException e) {
+            // Another CREATE TABLE may have initialized the same managed 
location and won the
+            // metastore race. Its catalog entry now owns .hoodie, so deleting 
it would corrupt the
+            // live table. A different location still belongs to this failed 
attempt and is safe to
+            // clean up.
+            if (!isTableRegisteredAtLocation(schemaTableName, basePath, e)) {
+                cleanupTableMetadata(fileSystem, basePath, e);
+            }
+            throw e;
+        }
+        catch (RuntimeException e) {
+            // Initialization may have written some or all of .hoodie, but no 
catalog entry from
+            // this call references it. Left there it would make a retry fail 
the emptiness check.
+            //
+            // Only .hoodie is removed, never the base path: the base path may 
have been created by
+            // someone else, and this connector did not create it. On object 
storage there is no
+            // directory to delete in any case -- deleteDirectory removes the 
objects under the
+            // prefix, which is exactly the set initTable wrote or may have 
partially written.
+            cleanupTableMetadata(fileSystem, basePath, e);
+            throw e;
+        }
+    }
+
+    private boolean isTableRegisteredAtLocation(SchemaTableName tableName, 
String basePath, RuntimeException failure)

Review Comment:
   🤖 nit: this returns `true` when the lookup fails, so the name reads as a 
fact it can't actually establish. Could you rename to something like 
`mayBeOwnedByAnotherTable` (or `isStorageOwnershipUncertainOrClaimed`) so the 
call site's `!` reads as "safe to clean up"?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-trino/src/main/java/io/trino/plugin/hudi/HudiMetadata.java:
##########
@@ -304,6 +321,202 @@ public Optional<Object> getInfo(ConnectorSession session, 
ConnectorTableHandle t
         return Optional.of(new HudiTableInfo(table.getSchemaTableName(), 
table.getTableType().name(), table.getBasePath()));
     }
 
+    /**
+     * Creates an empty table: its {@code .hoodie} directory on storage and 
its catalog entry.
+     * <p>
+     * This is the single-call path only. {@code beginCreateTable}/{@code 
finishCreateTable} and a
+     * page sink are deliberately absent, so {@code CREATE TABLE AS} and 
{@code INSERT} still fail as
+     * unsupported; no rows are written here.
+     * <p>
+     * An explicit {@code location} makes the table external, matching Hudi's 
Spark SQL behaviour. An
+     * omitted one makes it managed, at {@code <schemaLocation>/<tableName>}, 
which is what decides
+     * whether {@code DROP TABLE} later deletes the data.
+     */
+    @Override
+    public void createTable(ConnectorSession session, ConnectorTableMetadata 
tableMetadata, SaveMode saveMode)
+    {
+        SchemaTableName schemaTableName = tableMetadata.getTable();
+        if (saveMode == SaveMode.REPLACE) {
+            // Replacing a table means deciding what happens to the rows 
already in it, which is the
+            // write path this connector does not yet have.
+            throw new TrinoException(NOT_SUPPORTED, "This connector does not 
support replacing tables");
+        }
+        Database database = 
metastore.getDatabase(schemaTableName.getSchemaName())
+                .orElseThrow(() -> new 
SchemaNotFoundException(schemaTableName.getSchemaName()));
+        if (metastore.getTable(schemaTableName.getSchemaName(), 
schemaTableName.getTableName()).isPresent()) {
+            if (saveMode == SaveMode.IGNORE) {
+                return;
+            }
+            throw new TrinoException(ALREADY_EXISTS, "Table already exists: " 
+ schemaTableName);
+        }
+
+        // Everything that can be rejected is rejected before storage is 
touched, so a bad statement
+        // leaves nothing behind.
+        HudiTableValidation.validateCreateTable(tableMetadata);
+
+        Map<String, Object> properties = tableMetadata.getProperties();
+        Optional<String> explicitLocation = getTableLocation(properties);
+        boolean external = explicitLocation.isPresent();
+        String basePath = explicitLocation.orElseGet(() -> 
defaultTableLocation(database, schemaTableName));
+
+        TrinoFileSystem fileSystem = fileSystemFactory.create(session);
+        checkLocationIsEmpty(fileSystem, basePath);
+
+        // One schema object produces both hoodie.table.create.schema and the 
metastore column list,
+        // so the two cannot disagree (HUDI-9435).
+        HoodieSchema tableSchema = HudiSchemaConverter.toTableSchema(
+                tableMetadata.getColumns(), schemaTableName.getTableName());
+        Table table = HudiMetastoreTables.buildTable(
+                schemaTableName,
+                basePath,
+                getTableType(properties),
+                tableSchema,
+                getPartitionedBy(properties),
+                external,
+                Optional.of(session.getUser()),
+                tableMetadata.getComment());
+        try {
+            HudiTableInitializer.initializeTable(fileSystem, basePath, 
tableMetadata, tableSchema);
+            metastore.createTable(table, NO_PRIVILEGES);
+        }
+        catch (TableAlreadyExistsException e) {
+            // Another CREATE TABLE may have initialized the same managed 
location and won the
+            // metastore race. Its catalog entry now owns .hoodie, so deleting 
it would corrupt the
+            // live table. A different location still belongs to this failed 
attempt and is safe to
+            // clean up.
+            if (!isTableRegisteredAtLocation(schemaTableName, basePath, e)) {
+                cleanupTableMetadata(fileSystem, basePath, e);
+            }
+            throw e;
+        }
+        catch (RuntimeException e) {
+            // Initialization may have written some or all of .hoodie, but no 
catalog entry from
+            // this call references it. Left there it would make a retry fail 
the emptiness check.
+            //
+            // Only .hoodie is removed, never the base path: the base path may 
have been created by
+            // someone else, and this connector did not create it. On object 
storage there is no
+            // directory to delete in any case -- deleteDirectory removes the 
objects under the
+            // prefix, which is exactly the set initTable wrote or may have 
partially written.
+            cleanupTableMetadata(fileSystem, basePath, e);
+            throw e;
+        }
+    }
+
+    private boolean isTableRegisteredAtLocation(SchemaTableName tableName, 
String basePath, RuntimeException failure)
+    {
+        try {
+            return metastore.getTable(tableName.getSchemaName(), 
tableName.getTableName())
+                    .map(table -> 
table.getStorage().getLocation().equals(basePath))

Review Comment:
   🤖 Could this string equality miss when the metastore normalizes the location 
it stores? Thrift HMS rewrites `sd.location` through `Warehouse.getDnsPath` on 
create (e.g. `file:///tmp/x` → `file:/tmp/x`, trailing slash stripped), so a 
winner at the *same* path could compare unequal here and the loser would then 
delete the winner's `.hoodie`. Comparing normalized paths (e.g. via 
`Location.of(...)`/`StoragePath`) or erring towards "registered" on any 
mismatch under the same table name might be safer, since the failure mode is 
destructive.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-trino/src/main/java/io/trino/plugin/hudi/HudiMetastoreTables.java:
##########
@@ -0,0 +1,133 @@
+/*
+ * Licensed 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.
+ */
+package io.trino.plugin.hudi;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import io.trino.metastore.Column;
+import io.trino.metastore.StorageFormat;
+import io.trino.metastore.Table;
+import io.trino.spi.TrinoException;
+import io.trino.spi.connector.SchemaTableName;
+import org.apache.hudi.common.model.HoodieTableType;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaField;
+import org.apache.hudi.sync.common.util.HoodieMetastoreTableDescriptor;
+
+import java.util.List;
+import java.util.Optional;
+
+import static io.trino.plugin.hive.TableType.EXTERNAL_TABLE;
+import static io.trino.plugin.hive.TableType.MANAGED_TABLE;
+import static io.trino.plugin.hudi.HudiUtil.toColumnHandle;
+import static io.trino.spi.StandardErrorCode.INVALID_TABLE_PROPERTY;
+
+/**
+ * Translates {@link HoodieMetastoreTableDescriptor} into a Trino {@link 
Table}.
+ * <p>
+ * The assembly decisions -- which input format carries the table type, which 
columns are partition
+ * columns, which properties make the table recognisable to Spark SQL, where
+ * {@code serialization.format} goes -- are not made here. They live in {@code 
hudi-sync-common}
+ * next to {@code SparkDataSourceTableUtils}, in Hudi and JDK types, so that 
the normalization contract
+ * is reusable by metastore integrations without depending on Trino types. All 
that happens here is
+ * the mapping into {@code io.trino.metastore} types. Parity tests in {@code 
hudi-hive-sync} pin the
+ * shared format names and schema-splitting default to the values its existing 
executor emits.
+ * <p>
+ * The columns come from the same {@link HoodieSchema} that becomes
+ * {@code hoodie.table.create.schema}, which is the one thing this class does 
insist on. The
+ * connector keeps two independent descriptions of a table's columns -- that 
schema, and the
+ * metastore storage descriptor -- and {@link HudiMetadata#getColumnHandles} 
reads only the latter.
+ * Nothing reconciles them, so a table built from two separately-handled 
inputs can end up registered
+ * with columns that do not match its own schema, or with none at all 
(HUDI-9435:
+ * {@code SELECT * not allowed from relation that has no columns}). Both 
{@code CREATE TABLE} and
+ * {@code register_table} go through here so that cannot happen.
+ * <p>
+ * Column types come from {@link HudiUtil#toColumnHandle}, the same 
Avro-to-Trino mapping the read
+ * path uses, so the types registered are the types a query will see.
+ */
+public final class HudiMetastoreTables
+{
+    private HudiMetastoreTables() {}
+
+    /**
+     * Builds the metastore descriptor for a table's snapshot view.
+     * <p>
+     * Lenient about a partition column that is absent from {@code 
tableSchema}, typing it as a
+     * string, because the shared layer is and because {@code register_table} 
has to cope with real
+     * tables built by key generators that do not record their partition 
fields in the schema.
+     * {@code CREATE TABLE} rejects that case earlier, in {@link 
HudiTableValidation}, where the
+     * offending property can be named.
+     *
+     * @param tableSchema the table's Hudi schema, including the meta fields; 
supplies both the
+     *         column names and their types
+     * @param partitionedBy partition column names, in partition-path order
+     * @param external whether the table is external. An explicit {@code 
location} makes a table
+     *         external, an omitted one makes it managed; see {@link 
HudiMetadata#createTable}.
+     */
+    public static Table buildTable(
+            SchemaTableName tableName,
+            String basePath,
+            HoodieTableType tableType,
+            HoodieSchema tableSchema,
+            List<String> partitionedBy,
+            boolean external,
+            Optional<String> owner,
+            Optional<String> comment)
+    {
+        HoodieMetastoreTableDescriptor descriptor;
+        try {
+            descriptor = HoodieMetastoreTableDescriptor.forSnapshotView(
+                    tableSchema, partitionedBy, tableType, basePath, external);
+        }
+        catch (IllegalArgumentException e) {
+            throw new TrinoException(INVALID_TABLE_PROPERTY, e.getMessage(), 
e);
+        }
+
+        ImmutableMap.Builder<String, String> parameters = 
ImmutableMap.<String, String>builder()
+                .putAll(descriptor.getTableParameters());
+        comment.ifPresent(value -> parameters.put(Table.TABLE_COMMENT, value));
+
+        return Table.builder()
+                .setDatabaseName(tableName.getSchemaName())
+                .setTableName(tableName.getTableName())
+                // Hive treats the table type and the EXTERNAL parameter as 
independent; the shared
+                // layer supplies the parameter, this supplies the type, and 
they must agree.
+                .setTableType((external ? EXTERNAL_TABLE : 
MANAGED_TABLE).name())
+                .setOwner(owner)
+                .setDataColumns(toColumns(descriptor.getDataFields()))
+                
.setPartitionColumns(toColumns(descriptor.getPartitionFields()))
+                .setParameters(parameters.buildKeepingLast())
+                .withStorage(storage -> storage
+                        .setStorageFormat(StorageFormat.create(
+                                descriptor.getSerdeClassName(),
+                                descriptor.getInputFormatClassName(),
+                                descriptor.getOutputFormatClassName()))
+                        .setSerdeParameters(descriptor.getSerdeParameters())
+                        .setLocation(basePath))
+                .build();
+    }
+
+    private static List<Column> toColumns(List<HoodieSchemaField> fields)
+    {
+        ImmutableList.Builder<Column> columns = 
ImmutableList.builderWithExpectedSize(fields.size());
+        for (HoodieSchemaField field : fields) {
+            columns.add(new Column(
+                    field.name(),
+                    toColumnHandle(field).getHiveType(),
+                    field.doc().isPresent() ? Optional.of(field.doc().get()) : 
Optional.empty(),

Review Comment:
   🤖 nit: `field.doc().isPresent() ? Optional.of(field.doc().get()) : 
Optional.empty()` could be `field.doc().toJavaOptional()` — Hudi's `Option` 
already provides that conversion.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to