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


##########
src/paimon/core/operation/append_only_file_store_write.cpp:
##########
@@ -278,9 +299,25 @@ Result<AppendOnlyFileStoreWrite::WriterFactory> 
AppendOnlyFileStoreWrite::GetDat
         data_file_path_factory, pool_);
 }
 
+Result<bool> AppendOnlyFileStoreWrite::CanUseDictionaryPassthrough(
+    const std::shared_ptr<ShreddingWritePlanFactory>& plan_factory) const {
+    std::shared_ptr<FileFormat> file_format = options_.GetFileFormat();
+    if (!file_format || file_format->Identifier() != "parquet") {
+        return false;
+    }
+    PAIMON_ASSIGN_OR_RAISE(
+        bool enable_dictionary,
+        OptionsUtils::GetValueFromMap<bool>(options_.ToMap(), 
parquet::PARQUET_ENABLE_DICTIONARY,
+                                            
::parquet::DEFAULT_IS_DICTIONARY_ENABLED));
+    if (!enable_dictionary) {

Review Comment:
   Fair point, and it was a real layering break: 
`append_only_file_store_write.cpp` was the only file outside 
`src/paimon/format/parquet/` including `parquet_format_defs.h`, plus it pulled 
in `parquet/properties.h` for the default.
   
   Both includes are gone. The option names and their defaults are now local 
`constexpr` in an anonymous namespace in that `.cpp`, so core links no Parquet 
symbols at all and a replacement Parquet plugin cannot break the build here. 
The file already compared the format identifier against a plain `"parquet"` 
literal, so this is consistent with what was there.
   
   One small deviation from the literal suggestion: I used named constants 
rather than inlining the strings, because 
`parquet.read.enable-dictionary-passthrough` is needed in two functions and 
writing it twice invites a typo. They are string literals in the same 
translation unit, so the dependency is gone either way. Happy to inline them if 
you would rather.
   



##########
src/paimon/format/parquet/parquet_format_writer.cpp:
##########
@@ -74,6 +81,51 @@ Status ParquetFormatWriter::AddBatch(ArrowArray* batch) {
     return Status::OK();
 }
 
+Result<std::shared_ptr<arrow::Schema>> ParquetFormatWriter::ResolveBatchSchema(
+    const ::ArrowArray* batch) {
+    PAIMON_ASSIGN_OR_RAISE(
+        std::shared_ptr<arrow::DataType> batch_type,
+        ArrowUtils::ResolveParquetDictionaryStructType(logical_struct_type_, 
batch));
+    if (batch_type == logical_struct_type_) {
+        return schema_;
+    }
+    if (dictionary_batch_type_ == nullptr || 
!dictionary_batch_type_->Equals(*batch_type)) {
+        dictionary_batch_type_ = batch_type;
+        dictionary_batch_schema_ = arrow::schema(batch_type->fields(), 
schema_->metadata());
+    }
+    return dictionary_batch_schema_;

Review Comment:
   No, it was not a measured hotspot — I had reasoned about it rather than 
profiled it, which is not a good enough reason for mutable state on the writer. 
Both members are gone and `ResolveBatchSchema()` builds and returns the schema 
directly; it is `const` now as a result.
   
   Your second point is the stronger one: the cache was keyed on the batch 
type, so an alternating encoding would have rebuilt it on every batch anyway, 
and it added a way for the cached schema to drift from the batch it describes. 
Velox has to reconcile exactly that (it caches its Arrow schema for field-id 
fixups and then rewrites cached fields when a column's encoding changes across 
batches); rebuilding per batch means there is no such window here.
   



##########
src/paimon/format/parquet/parquet_format_writer.cpp:
##########
@@ -74,6 +81,51 @@ Status ParquetFormatWriter::AddBatch(ArrowArray* batch) {
     return Status::OK();
 }
 
+Result<std::shared_ptr<arrow::Schema>> ParquetFormatWriter::ResolveBatchSchema(
+    const ::ArrowArray* batch) {
+    PAIMON_ASSIGN_OR_RAISE(
+        std::shared_ptr<arrow::DataType> batch_type,
+        ArrowUtils::ResolveParquetDictionaryStructType(logical_struct_type_, 
batch));
+    if (batch_type == logical_struct_type_) {
+        return schema_;
+    }
+    if (dictionary_batch_type_ == nullptr || 
!dictionary_batch_type_->Equals(*batch_type)) {
+        dictionary_batch_type_ = batch_type;
+        dictionary_batch_schema_ = arrow::schema(batch_type->fields(), 
schema_->metadata());
+    }
+    return dictionary_batch_schema_;
+}
+
+Result<std::shared_ptr<arrow::RecordBatch>> 
ParquetFormatWriter::FlattenUnwritableDictionaries(
+    const std::shared_ptr<arrow::RecordBatch>& record_batch) const {
+    arrow::ArrayVector columns;
+    arrow::FieldVector fields;
+    arrow::compute::ExecContext exec_context(pool_.get());
+    for (int32_t i = 0; i < record_batch->num_columns(); ++i) {
+        const std::shared_ptr<arrow::Array>& column = record_batch->column(i);
+        if (column->type_id() != arrow::Type::DICTIONARY ||
+            checked_cast<const 
arrow::DictionaryArray&>(*column).dictionary()->null_count() == 0) {
+            continue;
+        }
+        if (columns.empty()) {
+            columns = record_batch->columns();
+            fields = record_batch->schema()->fields();
+        }
+        const auto& dictionary_type = checked_cast<const 
arrow::DictionaryType&>(*column->type());
+        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+            arrow::Datum flattened,
+            arrow::compute::Cast(column, dictionary_type.value_type(),
+                                 arrow::compute::CastOptions::Safe(), 
&exec_context));

Review Comment:
   Done — `FlattenUnwritableDictionaries()` uses `CastingUtils::Cast()` with 
the writer's pool, so the local `arrow::compute::ExecContext` is gone here too.
   



##########
test/inte/append_compaction_inte_test.cpp:
##########
@@ -822,4 +824,239 @@ TEST_F(AppendCompactionInteTest, 
TestAppendTableCompactionWithIOException) {
     ASSERT_TRUE(compaction_run_complete);
 }
 
+// Rewriting through the dictionary passthrough has to produce the same table 
as rewriting through
+// materialized values, whatever encoding each input file happens to carry. 
The interesting part is
+// the chain the unit tests cannot reach on their own: CompactRewrite hands 
the batch to the file
+// index writer and to the format writer, and both of them recover each 
column's encoding from the
+// batch layout after the type has been dropped by the C data interface.
+//
+// Parameterised over the two formats that have an encoding to forward or to 
suppress: Parquet
+// turns the passthrough on, ORC forces it off because its writer cannot take 
a dictionary-encoded
+// batch. ORC lazy decoding is on throughout, which makes the ORC reader hand 
over
+// `dictionary(int64, large_utf8)` - a shape no layout can resolve, so it 
exercises the
+// decode-at-the-source path rather than the passthrough.
+TEST_P(AppendCompactionInteTest, 
TestAppendTableCompactionDictionaryPassthrough) {
+    auto file_format = GetParam();
+    if (file_format != "parquet" && file_format != "orc") {
+        GTEST_SKIP() << file_format << " has no dictionary encoding to forward 
or to suppress";
+    }
+    auto dir = UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+
+    // `s` and `b` are low-cardinality and come back encoded; `id` is INT32, 
which the gate excludes
+    // by physical type, so the rewrite carries both kinds of column at once. 
`u` holds a distinct
+    // value per row, the shape passthrough saves least on. The 
cardinality-driven half of the gate
+    // - a column that starts dictionary-encoded and falls back to plain 
partway through a file -
+    // needs more rows than a readable fixture holds and is covered by
+    // 
ParquetFileBatchReaderTest.TestDictionaryPassthroughSkipsFallbackToPlain 
instead.
+    arrow::FieldVector fields = {
+        arrow::field("id", arrow::int32()), arrow::field("s", arrow::utf8()),
+        arrow::field("b", arrow::binary()), arrow::field("u", arrow::utf8())};
+    auto schema = arrow::schema(fields);
+
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, file_format},
+        {Options::BUCKET, "1"},
+        {Options::BUCKET_KEY, "id"},
+        {Options::FILE_SYSTEM, "local"},
+        {"orc.read.enable-lazy-decoding", "true"},
+        // Above the distinct/total ratio of every column here, so ORC 
dictionary-encodes rather

Review Comment:
   Thanks — and please do. The ORC path here only exercises it from the append 
side: with `orc.read.enable-lazy-decoding` on, the reader hands over 
`dictionary(int64, large_utf8)`, whose index and offset widths no `ArrowArray` 
layout can report, so `CompactRewrite` decodes it at the source through 
`FlattenUnresolvableDictionaries()` while the type is still known. 
`AppendCompactionInteTest.TestAppendTableCompactionDictionaryPassthrough` is 
parameterised over ORC precisely to keep that path covered. If the PK fix needs 
the same decode-before-export step, that helper is reusable as is.
   



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