lxy-9602 commented on code in PR #438:
URL: https://github.com/apache/paimon-cpp/pull/438#discussion_r4312972867
##########
src/paimon/core/core_options.cpp:
##########
@@ -599,6 +601,15 @@ struct CoreOptions::Impl {
// Parse file.format - data file format, default "parquet"
PAIMON_RETURN_NOT_OK(parser.ParseObject<FileFormatFactory>(
Options::FILE_FORMAT, /*default_identifier=*/"parquet",
&file_format));
+ if (parser.ContainsKey(Options::VECTOR_FILE_FORMAT)) {
+ PAIMON_RETURN_NOT_OK(parser.ParseObject<FileFormatFactory>(
+ Options::VECTOR_FILE_FORMAT, /*default_identifier=*/"vortex",
&vector_file_format));
+ }
+ PAIMON_RETURN_NOT_OK(
+ parser.ParseMemorySize(Options::VECTOR_TARGET_FILE_SIZE,
&vector_target_file_size));
+ if (vector_target_file_size && vector_target_file_size.value() <= 0) {
+ return Status::Invalid("vector.target-file-size must be positive");
+ }
Review Comment:
Please add tests for these two interfaces in `core_options_test.cpp`. May
only need to add two lines for `TestDefaultValue` and `TestFromMap`.
##########
test/test_data/vortex/dedicated_vector_java_rolling.db/dedicated_vector_java_rolling/README.md:
##########
@@ -0,0 +1,128 @@
+<!--
+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.
+-->
+
Review Comment:
Please refer to the `README` of other test data and align the format.
##########
test/test_data/vortex/dedicated_vector_java_rolling.db/dedicated_vector_java_rolling/GenerateDedicatedVectorRollingFixture.java:
##########
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+
+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.data.BinaryString;
+import org.apache.paimon.data.BinaryVector;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.manifest.ManifestEntry;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
+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.source.ReadBuilder;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.utils.CloseableIterator;
+
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** Generates one Java main file with independently rolled dedicated VECTOR
files. */
Review Comment:
Please remove this Java file and the pom file. We want the data construction
pattern to be understandable via the README alone.
##########
src/paimon/core/io/rolling_vector_file_writer.cpp:
##########
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+
+#include "paimon/core/io/rolling_vector_file_writer.h"
+
+#include "arrow/array/array_nested.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/checked_cast.h"
+
+namespace paimon {
+
+RollingVectorFileWriter::RollingVectorFileWriter(
+ int64_t target_file_size, int64_t target_file_row_num, int64_t
vector_target_file_size,
+ const std::shared_ptr<WriterFactory>& main_factory,
+ const std::shared_ptr<WriterFactory>& vector_factory,
+ const std::shared_ptr<arrow::Schema>& write_schema, int32_t vector_index)
+ : RollingFileWriter(target_file_size, target_file_row_num, main_factory),
+ vector_target_file_size_(vector_target_file_size),
+ vector_factory_(vector_factory),
+ write_schema_(write_schema),
+ vector_index_(vector_index) {}
+
+Status RollingVectorFileWriter::Write(::ArrowArray* record) {
+ if (closed_ || aborted_) {
+ return Status::Invalid("Writer has already closed or aborted!");
+ }
+ ScopeGuard guard([this]() { Abort(); });
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::shared_ptr<arrow::Array> array,
+ arrow::ImportArray(record, arrow::struct_(write_schema_->fields())));
+ if (!array || array->type_id() != arrow::Type::STRUCT ||
array->null_count() != 0) {
+ return Status::Invalid("Dedicated VECTOR storage expects non-null
struct rows");
+ }
+ if (array->length() == 0) {
+ guard.Release();
+ return Status::OK();
+ }
+ if (!current_writer_) {
+ PAIMON_RETURN_NOT_OK(OpenCurrentWriter());
+ PAIMON_ASSIGN_OR_RAISE(vector_writer_,
vector_factory_->CreateWriter());
+ }
+ auto rows = checked_pointer_cast<arrow::StructArray>(array);
+ arrow::ArrayVector main_arrays;
+ arrow::FieldVector main_fields;
+ for (int32_t i = 0; i < write_schema_->num_fields(); ++i) {
+ if (i != vector_index_) {
+ main_arrays.push_back(rows->field(i));
+ main_fields.push_back(write_schema_->field(i));
+ }
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray>
main_array,
+ arrow::StructArray::Make(main_arrays,
main_fields));
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::shared_ptr<arrow::StructArray> vector_array,
+ arrow::StructArray::Make({rows->field(vector_index_)},
+ {write_schema_->field(vector_index_)}));
+ ::ArrowArray main_batch = {};
+ ::ArrowArray vector_batch = {};
+ ScopeGuard arrays_guard([&]() {
+ ArrowArrayRelease(&main_batch);
+ ArrowArrayRelease(&vector_batch);
+ });
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*main_array,
&main_batch));
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*vector_array,
&vector_batch));
+ PAIMON_RETURN_NOT_OK(current_writer_->Write(&main_batch));
+ PAIMON_RETURN_NOT_OK(vector_writer_->Write(&vector_batch));
+ record_count_ += rows->length();
+ current_file_record_count_ += rows->length();
+
+ const bool suggested_check = SuggestCheck();
+ PAIMON_ASSIGN_OR_RAISE(bool main_full,
+ current_writer_->ReachTargetSize(suggested_check,
target_file_size_));
+ PAIMON_ASSIGN_OR_RAISE(bool vector_full, vector_writer_->ReachTargetSize(
+ suggested_check,
vector_target_file_size_));
+ if (main_full || vector_full || current_file_record_count_ >=
target_file_row_num_) {
+ PAIMON_RETURN_NOT_OK(CloseCurrentPair());
Review Comment:
This behavior isn’t consistent with Java. In Java, the VECTOR writer itself
is a `RollingFileWriterImpl`: when the VECTOR size reaches its target, it only
rolls the VECTOR files; the VECTOR writer is only closed when the main file
rolls. However, the current C++ implementation closes both sides together
whenever either side hits its size/line targets.
##########
test/inte/blob_table_inte_test.cpp:
##########
@@ -1894,12 +1894,37 @@ TEST_P(BlobTableInteTest,
TestDataEvolutionBlobPartialUpdateMultipleLayers) {
const std::vector<std::string> expected_rows = {
R"([[1, "a", "update1_0"]])", R"([[2, "b", "blob_1"]])", R"([[3, "c",
"blob_2"]])",
R"([[4, "d", "update2_3"]])"};
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> unpruned_plan,
ScanTable(table_path));
+ ASSERT_EQ(unpruned_plan->Splits().size(), 1);
+ auto unpruned_split =
std::dynamic_pointer_cast<DataSplitImpl>(unpruned_plan->Splits()[0]);
+ ASSERT_NE(unpruned_split, nullptr);
+ ASSERT_EQ(unpruned_split->DataFiles().size(), 4);
for (int32_t i = 0; i < static_cast<int32_t>(expected_rows.size()); i++) {
+ SCOPED_TRACE(i);
auto expected_single = std::dynamic_pointer_cast<arrow::StructArray>(
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields),
expected_rows[i])
.ValueOrDie());
ASSERT_OK(ScanAndRead(table_path, schema->field_names(),
expected_single,
/*predicate=*/nullptr, /*row_ranges=*/{Range(i,
i)}));
+
+ // Range planning can narrow a read after scanning, leaving unselected
BLOB files
+ // in the split. Their sequence layers must still align with the
selected rows.
+ auto indexed_split =
+ std::make_shared<IndexedSplitImpl>(unpruned_split,
std::vector<Range>{Range(i, i)});
+ ReadContextBuilder read_builder(table_path);
+
read_builder.SetReadFieldNames(schema->field_names()).EnableLateMaterializing(false);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context,
read_builder.Finish());
Review Comment:
Can we just use the `ReadTable` function here?
##########
src/paimon/core/operation/data_evolution_split_read.cpp:
##########
@@ -674,7 +676,200 @@ DataEvolutionSplitRead::SplitFieldBunches(
return fields_files;
}
-Result<std::unique_ptr<DataEvolutionFileReader>>
DataEvolutionSplitRead::CreateUnionReader(
+Result<std::vector<DataEvolutionSplitRead::VectorReadRange>>
+DataEvolutionSplitRead::PlanVectorReadRanges(
+ const std::vector<std::shared_ptr<DataFileMeta>>& files,
+ const std::vector<int32_t>& read_field_ids,
+ const std::function<Result<std::vector<int32_t>>(const
std::shared_ptr<DataFileMeta>&)>&
+ file_to_field_ids) {
+ struct Candidate {
+ std::shared_ptr<DataFileMeta> file;
+ std::vector<int32_t> field_ids;
+ };
+ std::vector<Candidate> candidates;
+ std::vector<std::shared_ptr<DataFileMeta>> other_files;
+ std::map<int64_t, std::vector<size_t>> boundaries;
+ std::unordered_set<int32_t> projected_ids(read_field_ids.begin(),
read_field_ids.end());
+ std::optional<Range> logical_range;
+ int64_t first_row_id = std::numeric_limits<int64_t>::max();
+ int64_t end_row_id = std::numeric_limits<int64_t>::min();
+ bool vector_only = std::all_of(files.begin(), files.end(), [](const auto&
file) {
+ return VectorStoreUtils::IsVectorStoreFile(file->file_name);
+ });
+ for (const auto& file : files) {
+ PAIMON_ASSIGN_OR_RAISE(int64_t first, file->NonNullFirstRowId());
+ if (first < 0 || file->row_count <= 0 ||
+ first > std::numeric_limits<int64_t>::max() - file->row_count) {
+ return Status::Invalid("Invalid row id range in VECTOR merge
group");
+ }
+ int64_t end = first + file->row_count;
+ first_row_id = std::min(first_row_id, first);
+ end_row_id = std::max(end_row_id, end);
+ if (!VectorStoreUtils::IsVectorStoreFile(file->file_name)) {
+ other_files.push_back(file);
+ if (DataEvolutionUtils::IsNormalFile(file->file_name)) {
+ logical_range = Range(first, end - 1);
+ }
+ continue;
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::vector<int32_t> field_ids,
file_to_field_ids(file));
+ field_ids.erase(std::remove_if(field_ids.begin(), field_ids.end(),
+ [&](int32_t id) { return
projected_ids.count(id) == 0; }),
+ field_ids.end());
+ if (!field_ids.empty() || vector_only) {
+ size_t index = candidates.size();
+ candidates.push_back(Candidate{file, std::move(field_ids)});
+ boundaries[first].push_back(index);
+ boundaries[end].push_back(index);
+ }
+ }
+ if (files.empty()) {
+ return std::vector<VectorReadRange>();
+ }
+ if (!logical_range) {
+ logical_range = Range(first_row_id, end_row_id - 1);
+ }
+ boundaries[logical_range->from];
+ boundaries[logical_range->to + 1];
+
+ auto newest_first = [&](size_t left, size_t right) {
+ const auto& lhs = candidates[left].file;
+ const auto& rhs = candidates[right].file;
+ if (lhs->max_sequence_number != rhs->max_sequence_number) {
+ return lhs->max_sequence_number > rhs->max_sequence_number;
+ }
+ if (lhs->file_name != rhs->file_name) {
+ return lhs->file_name < rhs->file_name;
+ }
+ return left < right;
+ };
+ std::set<size_t, decltype(newest_first)> active(newest_first);
+ std::vector<VectorReadRange> result;
+ int64_t start = boundaries.begin()->first;
+ for (const auto& [end, events] : boundaries) {
+ int64_t from = std::max(start, logical_range->from);
+ int64_t to = std::min(end - 1, logical_range->to);
+ if (from <= to) {
+ std::unordered_set<int32_t> assigned;
+ std::vector<std::shared_ptr<DataFileMeta>> providers = other_files;
+ for (size_t index : active) {
+ const auto& candidate = candidates[index];
+ bool provides_field = false;
+ for (int32_t id : candidate.field_ids) {
+ provides_field |= assigned.insert(id).second;
+ }
+ // An explicit NULL in a newer file overrides the older value
as well.
+ if (provides_field) {
+ providers.push_back(candidate.file);
+ }
+ }
+ if (providers.empty() && vector_only && !active.empty()) {
+ providers.push_back(candidates[*active.begin()].file);
+ }
+ // Ordinary inline fields and dedicated fields share the same
sequence precedence.
+ // BLOB files remain last and retain their own per-row fallback
ordering.
+ std::stable_sort(
+ providers.begin(), providers.end(), [](const auto& lhs, const
auto& rhs) {
+ bool lhs_blob = BlobUtils::IsBlobFile(lhs->file_name);
+ bool rhs_blob = BlobUtils::IsBlobFile(rhs->file_name);
+ if (lhs_blob != rhs_blob) {
+ return !lhs_blob;
+ }
+ return !lhs_blob && lhs->max_sequence_number >
rhs->max_sequence_number;
+ });
+ if (!result.empty() && result.back().range.to + 1 == from &&
+ result.back().files == providers) {
+ result.back().range.to = to;
+ } else {
+ result.push_back(VectorReadRange{Range(from, to),
std::move(providers)});
+ }
+ }
+ for (size_t index : events) {
+ if (active.erase(index) == 0) {
+ active.insert(index);
+ }
+ }
+ start = end;
+ }
+ return result;
Review Comment:
For a single main file covered by multiple independently rolled VECTOR
files, this implementation appears to create a separate main-file reader for
each planned range when projecting both regular and vector columns. Java keeps
the sequential read path for contiguous, non-overlapping VECTOR files, avoiding
these repeated opens.
Could we at least add a code comment documenting this difference and a TODO
to preserve the sequential path for this layout? That would make the current
trade-off explicit and help track the follow-up optimization.
##########
src/paimon/core/schema/schema_validation.cpp:
##########
@@ -1261,6 +1263,58 @@ Status SchemaValidation::ValidateMapStorageLayout(const
TableSchema& schema,
return Status::OK();
}
+Status SchemaValidation::ValidateVectorStore(const TableSchema& schema,
+ const CoreOptions& options) {
+ const std::shared_ptr<FileFormat> vector_format =
options.GetVectorFileFormat();
+ if (!vector_format) {
+ return Status::OK();
+ }
+ if (vector_format->Identifier() != "vortex") {
+ return Status::NotImplemented("Dedicated VECTOR storage currently only
supports vortex");
Review Comment:
It seems Java’s independently configured dedicated VECTOR storage isn’t
limited to Vortex—Lance and even Parquet support it as well. Why do we want to
enforce a specific format here?
--
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]