SteNicholas commented on code in PR #222:
URL: https://github.com/apache/paimon-cpp/pull/222#discussion_r3954189749


##########
src/paimon/core/schema/schema_validation.cpp:
##########
@@ -134,27 +143,146 @@ bool SchemaValidation::IsComplexType(const 
std::shared_ptr<arrow::Field>& field)
             BlobUtils::IsBlobField(field));
 }
 
-Status SchemaValidation::ValidateTableSchema(const TableSchema& schema) {
-    const auto& field_names = schema.FieldNames();
-    PAIMON_RETURN_NOT_OK(ValidateNoDuplicateField(schema.BucketKeys(), "bucket 
key"));
-    PAIMON_RETURN_NOT_OK(ValidateNoDuplicateField(schema.PrimaryKeys(), 
"primary key"));
-    PAIMON_RETURN_NOT_OK(ValidateNoDuplicateField(schema.PartitionKeys(), 
"partition key"));
-    PAIMON_RETURN_NOT_OK(
-        Preconditions::CheckState(ObjectUtils::ContainsAll(field_names, 
schema.PartitionKeys()),
-                                  "Table column {} should include all 
partition fields {}",
-                                  field_names, schema.PartitionKeys()));
-    PAIMON_RETURN_NOT_OK(
-        Preconditions::CheckState(ObjectUtils::ContainsAll(field_names, 
schema.PrimaryKeys()),
-                                  "Table column {} should include all primary 
key constraint {}",
-                                  field_names, schema.PrimaryKeys()));
-
-    PAIMON_RETURN_NOT_OK(
-        ValidateOnlyContainPrimitiveType(schema.Fields(), 
schema.PrimaryKeys(), "primary key"));
-    PAIMON_RETURN_NOT_OK(
-        ValidateOnlyContainPrimitiveType(schema.Fields(), 
schema.PartitionKeys(), "partition"));
+Status SchemaValidation::ValidateGenericSchema(const std::vector<DataField>& 
fields,
+                                               const std::vector<std::string>& 
bucket_keys,
+                                               const std::vector<std::string>& 
primary_keys,
+                                               const std::vector<std::string>& 
partition_keys) {
+    std::vector<std::string> field_names;
+    field_names.reserve(fields.size());
+    for (const DataField& field : fields) {
+        field_names.push_back(field.Name());
+    }
+    PAIMON_RETURN_NOT_OK(ValidateNoDuplicateField(bucket_keys, "bucket key"));
+    PAIMON_RETURN_NOT_OK(ValidateNoDuplicateField(primary_keys, "primary 
key"));
+    PAIMON_RETURN_NOT_OK(ValidateNoDuplicateField(partition_keys, "partition 
key"));
+    PAIMON_RETURN_NOT_OK(Preconditions::CheckState(
+        ObjectUtils::ContainsAll(field_names, partition_keys),
+        "Table column {} should include all partition fields {}", field_names, 
partition_keys));
+    PAIMON_RETURN_NOT_OK(Preconditions::CheckState(
+        ObjectUtils::ContainsAll(field_names, primary_keys),
+        "Table column {} should include all primary key constraint {}", 
field_names, primary_keys));
+    for (const auto& field_name : field_names) {
+        if (SpecialFields::IsSystemField(field_name)) {
+            return Status::Invalid(
+                fmt::format("field name '{}' in schema cannot be special 
field.", field_name));
+        }
+    }
+    PAIMON_RETURN_NOT_OK(ValidateOnlyContainPrimitiveType(fields, 
primary_keys, "primary key"));
+    PAIMON_RETURN_NOT_OK(ValidateOnlyContainPrimitiveType(fields, 
partition_keys, "partition"));
     // TODO(lisizhuo.lsz): C++ Paimon do not support timestamp & decimal & 
float & double type in
     // partition keys for now.
-    PAIMON_RETURN_NOT_OK(ValidateNotContainSpecificType(schema.Fields(), 
schema.PartitionKeys()));
+    PAIMON_RETURN_NOT_OK(ValidateNotContainSpecificType(fields, 
partition_keys));
+    return Status::OK();
+}
+
+Status SchemaValidation::ValidateNewTableSchema(const TableSchema& schema) {
+    const std::map<std::string, std::string>& options = schema.Options();
+    PAIMON_ASSIGN_OR_RAISE(TableType table_type, 
TableTypeDefine::FromOptions(options));
+    if (table_type != TableType::TABLE && table_type != 
TableType::MATERIALIZED_TABLE &&
+        table_type != TableType::FORMAT_TABLE) {
+        // Quoted back rather than re-rendered from `table_type`, so the 
message says what was
+        // actually asked for.
+        auto type_iter = options.find(Options::TYPE);
+        return Status::NotImplemented(fmt::format(
+            "Cannot create a table whose '{}' is '{}': paimon-cpp does not 
implement this table "
+            "type.",
+            Options::TYPE, type_iter == options.end() ? std::string() : 
type_iter->second));
+    }
+    if (table_type == TableType::FORMAT_TABLE) {
+        PAIMON_RETURN_NOT_OK(ValidateGenericTableSchema(schema));
+        // At creation the schema's own options are the only ones there are, 
and `file-system` is
+        // resolved from them like every other option.
+        return ValidateFormatTableSchema(schema, schema.Options(), 
/*file_system=*/nullptr);

Review Comment:
   Done. `SchemaValidation::ValidateNewTableSchema()` now takes the caller's 
already resolved `FileSystem`, and `SchemaManager::CreateTable()` passes its 
own `file_system_` (`schema_manager.cpp:116`). `ValidateFormatTableSchema()` 
takes it too, so reading the options through `CoreOptions` there does not 
resolve `file-system` a second time — which would fail for a caller that handed 
its own file system over rather than naming one.



##########
src/paimon/core/table/format/format_path_validation.h:
##########
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "paimon/table/format/format_table.h"
+
+namespace paimon {
+
+/// Checks that a path a caller handed over really names something of this 
format table.
+///
+/// A split and a commit message both come back through an interface that 
takes the base type, so
+/// what arrives may belong to another plan, another table or a plan made 
before the files moved,
+/// and nothing further down re-checks the paths they name.
+class FormatPathValidation {
+ public:
+    FormatPathValidation() = delete;
+    ~FormatPathValidation() = delete;
+
+    /// Fails when `path` is not a file inside `location`. By path component, 
not by string
+    /// prefix: `<location>/../victim` starts with the location and still 
resolves outside it.
+    /// Only the path text is checked, so a symbolic link pointing out of the 
table is not caught.
+    static Status ValidatePathUnderLocation(const std::string& path, const 
std::string& location,
+                                            const std::string& what);

Review Comment:
   Done. Both sides are normalized before being compared. 
`ResolveComparablePath()` parses each with `PathUtil::ToPath()`, treats an 
empty scheme and `file` as the same file system, compares the scheme 
case-insensitively and the authority separately, and resolves a relative local 
path against the working directory exactly as `LocalFile` does when it opens 
one. The containment check then runs on the normalized inner path by whole path 
components, so `<location>/../victim` is still refused.
   
   `/tmp/table` therefore holds `file:///tmp/table/data.parquet`, and a table 
located at `./table` matches the absolute paths its own listing hands back. 
Covered by `TestTheTableLocationIsRecognisedHoweverItIsWritten` and 
`TestATableAtARelativeLocationIsWrittenAndReadBack`.



##########
src/paimon/core/table/format/format_table_commit.h:
##########
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "paimon/core/table/format/format_commit_message.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "paimon/table/format/format_table.h"
+
+namespace paimon {
+
+/// Publishes the files a `FormatTableWrite` produced, by renaming each out of 
the `_temporary`
+/// directory it was staged in.
+///
+/// A directory has no metadata to switch, so a commit is not atomic across 
files: it renames them
+/// one at a time, and a reader scanning midway sees the ones renamed so far. 
Each rename does
+/// guarantee that a file becomes visible whole, never half-written.
+///
+/// A commit that fails partway tries to remove the files it had already 
renamed, on a best-effort
+/// basis: a file that cannot be removed is reported in the log and stays. An 
overwriting commit is
+/// further limited - the data it replaces is deleted before the new files are 
published and cannot
+/// be brought back, so a failure there leaves the table without the replaced 
data.
+///
+/// Only the messages this job's own writers produced may be passed in. The 
checks here can tell
+/// that a message's path belongs to this table, sits in the partition it 
declares, and is staged
+/// rather than already published - not whose staged file it is.
+///
+/// Not thread-safe. Separate commits may add to one table at once, each 
publishing only the files
+/// its own messages name; two overwriting commits over the same directory 
race, since an overwrite
+/// clears everything committed there before publishing anything.
+class FormatTableCommit {
+ public:
+    /// @param table Table to commit to.
+    /// @param overwrite Whether the commit replaces the data already in the 
directories it writes
+    ///        to, instead of adding to it. Without a static partition that 
means every partition
+    ///        the commit touches; with one it means the partitions that spec 
covers, whether or
+    ///        not this commit wrote to them.
+    /// @param static_partition Partition the commit writes to, keyed by 
partition field name. It
+    ///        may name only the leading partition keys, in which case it 
stands for every
+    ///        partition below that prefix. Empty means the partitions are 
whatever the written
+    ///        files say they are.
+    static Result<std::unique_ptr<FormatTableCommit>> Create(
+        const std::shared_ptr<FormatTable>& table, bool overwrite,
+        const std::map<std::string, std::string>& static_partition);
+
+    ~FormatTableCommit();
+
+    /// Renames every written file into place, first clearing what it replaces 
when the commit
+    /// overwrites.
+    Status Commit(const std::vector<FormatCommitMessage>& commit_messages);

Review Comment:
   Supported rather than refused. `FormatTableCommit` reads 
`dynamic-partition-overwrite` through `CoreOptions` once at `Create()`, and 
`ReplacesOnlyWrittenPartitions()` is true only when the option is on **and** 
the table is partitioned. Otherwise an overwrite clears everything the table 
holds — the condition Java's `FormatTableCommit` applies — so an overwrite 
whose query returns nothing still empties the table instead of leaving stale 
data behind.
   
   Both branches have tests, including the empty-message case and a table whose 
data directory has not been created yet.



##########
src/paimon/core/table/format/format_table_file_store_commit.h:
##########
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "paimon/core/table/format/format_commit_message.h"
+#include "paimon/defs.h"
+#include "paimon/file_store_commit.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+class FormatTable;
+class Metrics;
+
+/// Commits a format table through the `FileStoreCommit` interface, so that a 
caller holding a
+/// table path commits it the way it commits any other table. Java Paimon does 
the same through
+/// `FormatTable.newBatchWriteBuilder()`.
+///
+/// Most of `FileStoreCommit` is about snapshots and manifests, which a format 
table has none of:
+/// expiring them, rolling back to one, and filtering by a commit identifier 
recorded in one all
+/// refer to state this table does not keep. Each is refused rather than 
quietly doing nothing, so
+/// a caller moving between table types finds out at the call rather than from 
a table that did
+/// not change. `RowIdCheckConflict()` is the one exception, since it returns 
a reference and has
+/// no way to report a refusal.
+///
+/// What is left is what a directory of files can do: `Commit()`, 
`Overwrite()` and `Abort()`.
+class FormatTableFileStoreCommit : public FileStoreCommit {
+ public:
+    static Result<std::unique_ptr<FormatTableFileStoreCommit>> Create(
+        const std::shared_ptr<FormatTable>& table);
+
+    ~FormatTableFileStoreCommit() override;
+
+    Status Commit(const std::vector<std::shared_ptr<CommitMessage>>& 
commit_messages,
+                  int64_t commit_identifier = BATCH_WRITE_COMMIT_IDENTIFIER,
+                  std::optional<int64_t> watermark = std::nullopt) override;

Review Comment:
   Done here, and applied to the rest of the new surface: the two public 
`FormatTable::Create()` overloads in 
`include/paimon/table/format/format_table.h` no longer default 
`location_carries_paimon_metadata` or `dynamic_options`, and every caller — 
production and test — passes them explicitly.



-- 
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