Skip to content

Commit 4785a2f

Browse files
authored
Merge branch 'main' into codex/fix-review-minor-issues
2 parents 9dcce16 + 8848b21 commit 4785a2f

101 files changed

Lines changed: 6001 additions & 824 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmake_modules/CorrosionFetch.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# targets. Used to bring in third_party/tantivy_ffi for the tantivy-fulltext
1717
# global index (see docs/dev/tantivy_fts_migration_plan.md).
1818
#
19-
# Pinned to v0.5.0 (stable release). Requires CMake >= 3.22.
19+
# Pinned to v0.5.2 (stable release). Requires CMake >= 3.22.
2020

2121
include(FetchContent)
2222

include/paimon/read_context.h

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <string>
2424
#include <vector>
2525

26+
#include "arrow/c/abi.h"
2627
#include "paimon/cache/cache.h"
2728
#include "paimon/predicate/predicate.h"
2829
#include "paimon/result.h"
@@ -44,7 +45,7 @@ class FileSystem;
4445
class PAIMON_EXPORT ReadContext {
4546
public:
4647
ReadContext(const std::string& path, const std::string& branch,
47-
const std::vector<std::string>& read_schema,
48+
const std::vector<std::string>& read_field_names,
4849
const std::vector<int32_t>& read_field_ids,
4950
const std::shared_ptr<Predicate>& predicate, bool enable_predicate_filter,
5051
bool enable_prefetch, uint32_t prefetch_batch_count,
@@ -75,8 +76,8 @@ class PAIMON_EXPORT ReadContext {
7576
return options_;
7677
}
7778

78-
const std::vector<std::string>& GetReadSchema() const {
79-
return read_schema_;
79+
const std::vector<std::string>& GetReadFieldNames() const {
80+
return read_field_names_;
8081
}
8182

8283
const std::vector<int32_t>& GetReadFieldIds() const {
@@ -130,10 +131,26 @@ class PAIMON_EXPORT ReadContext {
130131
return cache_;
131132
}
132133

134+
/// Whether a read schema (C ArrowSchema) for nested column pruning was provided.
135+
bool HasReadSchema() const {
136+
return read_schema_ != nullptr && read_schema_->release != nullptr;
137+
}
138+
139+
/// Get the read schema as a mutable C ArrowSchema pointer.
140+
/// ImportSchema will consume (release) the schema content.
141+
ArrowSchema* GetReadSchema() {
142+
return read_schema_.get();
143+
}
144+
145+
/// Set the read schema from a C ArrowSchema unique_ptr and take ownership of
146+
/// schema resources (released via ArrowSchema::release in destructor).
147+
/// Called internally by ReadContextBuilder.
148+
void SetReadSchema(std::unique_ptr<ArrowSchema> schema);
149+
133150
private:
134151
std::string path_;
135152
std::string branch_;
136-
std::vector<std::string> read_schema_;
153+
std::vector<std::string> read_field_names_;
137154
std::vector<int32_t> read_field_ids_;
138155
std::shared_ptr<Predicate> predicate_;
139156
bool enable_predicate_filter_;
@@ -151,6 +168,8 @@ class PAIMON_EXPORT ReadContext {
151168
PrefetchCacheMode prefetch_cache_mode_;
152169
CacheConfig cache_config_;
153170
std::shared_ptr<Cache> cache_;
171+
// Owns schema resources and releases ArrowSchema::release in destructor.
172+
std::unique_ptr<ArrowSchema> read_schema_;
154173
};
155174

156175
/// `ReadContextBuilder` used to build a `ReadContext`, has input validation.
@@ -173,9 +192,9 @@ class PAIMON_EXPORT ReadContextBuilder {
173192
///
174193
/// @param read_field_names Vector of field names to read from the table.
175194
/// @return Reference to this builder for method chaining.
176-
/// @note Currently supports top-level field selection. Future versions may support
177-
/// nested field selection using ArrowSchema for more granular projection
178-
ReadContextBuilder& SetReadSchema(const std::vector<std::string>& read_field_names);
195+
/// @note Currently supports top-level field selection. For nested field selection
196+
/// use SetReadSchema(std::unique_ptr<ArrowSchema>) instead.
197+
ReadContextBuilder& SetReadFieldNames(const std::vector<std::string>& read_field_names);
179198
/// Set the schema fields to read from the table.
180199
///
181200
/// If not set, all fields from the table schema will be read. This is useful for
@@ -184,12 +203,51 @@ class PAIMON_EXPORT ReadContextBuilder {
184203
///
185204
/// @param read_field_ids Vector of field ids to read from the table.
186205
/// @return Reference to this builder for method chaining.
187-
/// @note Currently supports top-level field selection. Future versions may support
188-
/// nested field selection using ArrowSchema for more granular projection.
189-
/// @note SetReadFieldIds() and SetReadSchema() are mutually exclusive.
190-
/// Calling both will ignore the read schema set by SetReadSchema().
206+
/// @note Currently supports top-level field selection.
207+
/// @note SetReadFieldIds() and SetReadFieldNames() are mutually exclusive.
208+
/// Calling both will ignore the read schema set by SetReadFieldNames().
191209
ReadContextBuilder& SetReadFieldIds(const std::vector<int32_t>& read_field_ids);
192210

211+
/// Set the read Arrow Schema for nested column pruning.
212+
///
213+
/// The read schema is an Arrow C Data Interface schema where STRUCT types
214+
/// may contain only a subset of the original sub-fields, enabling nested column
215+
/// pruning to reduce I/O. Field matching is based on field name: the system
216+
/// looks up each field by name in the table schema and rebuilds the aligned
217+
/// schema using the table schema's type and metadata. Metadata propagation
218+
/// from the user-provided schema is whitelist-based: currently only
219+
/// "paimon.map.selected-keys" is preserved and merged into the final aligned
220+
/// schema.
221+
///
222+
/// To prune map entries by key, attach metadata "paimon.map.selected-keys"
223+
/// to the target map field in read schema. The value is a comma-separated
224+
/// key list, for example: "k1,k2". Only map fields with string key type
225+
/// (Arrow utf8) are supported.
226+
///
227+
/// Example:
228+
/// @code{.cpp}
229+
/// auto map_field = arrow::field("m", arrow::map(arrow::utf8(), arrow::int32()));
230+
/// auto map_meta = arrow::KeyValueMetadata::Make(
231+
/// {"paimon.map.selected-keys"}, {"k1,k2"});
232+
/// auto projected_schema = arrow::schema({
233+
/// arrow::field("id", arrow::int64()),
234+
/// map_field->WithMetadata(map_meta),
235+
/// });
236+
///
237+
/// auto c_schema = std::make_unique<ArrowSchema>();
238+
/// arrow::ExportSchema(*projected_schema, c_schema.get());
239+
///
240+
/// ReadContextBuilder builder("/path/to/table");
241+
/// builder.SetReadSchema(std::move(c_schema));
242+
/// @endcode
243+
///
244+
/// @param read_schema Arrow C Schema. Ownership of schema resources is transferred
245+
/// to the built ReadContext.
246+
/// @return Reference to this builder for method chaining.
247+
/// @note Priority: read_schema > read_field_ids > read_field_names.
248+
/// When set, read_field_ids and read_field_names are ignored.
249+
ReadContextBuilder& SetReadSchema(std::unique_ptr<ArrowSchema> read_schema);
250+
193251
/// Set a configuration options map to set some option entries which are not defined in the
194252
/// table schema or whose values you want to overwrite.
195253
/// @note The options map will clear the options added by `AddOption()` before.

src/paimon/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,24 @@ set(PAIMON_CORE_SRCS
220220
core/io/data_file_meta.cpp
221221
core/io/data_file_meta_serializer.cpp
222222
core/io/data_file_path_factory.cpp
223+
core/io/append_data_file_writer_factory.cpp
224+
core/io/blob_data_file_writer_factory.cpp
225+
core/io/data_file_writer_factory.cpp
223226
core/io/data_file_writer.cpp
224227
core/io/field_mapping_reader.cpp
225228
core/io/complete_row_tracking_fields_reader.cpp
226229
core/io/file_index_evaluator.cpp
227230
core/io/key_value_data_file_record_reader.cpp
231+
core/io/key_value_data_file_writer_factory.cpp
228232
core/io/key_value_data_file_writer.cpp
229233
core/io/key_value_in_memory_record_reader.cpp
230234
core/io/merged_key_value_record_reader.cpp
231235
core/io/key_value_meta_projection_consumer.cpp
232236
core/io/key_value_projection_consumer.cpp
233237
core/io/key_value_projection_reader.cpp
238+
core/io/map_shared_shredding_core_utils.cpp
239+
core/io/shredding_append_data_file_writer_factory.cpp
240+
core/io/shredding_key_value_data_file_writer_factory.cpp
234241
core/io/external_storage_blob_writer.cpp
235242
core/io/multiple_blob_file_writer.cpp
236243
core/io/rolling_blob_file_writer.cpp
@@ -343,6 +350,7 @@ set(PAIMON_CORE_SRCS
343350
core/utils/blob_view_lookup.cpp
344351
core/utils/consumer_manager.cpp
345352
core/utils/field_mapping.cpp
353+
core/utils/nested_projection_utils.cpp
346354
core/utils/file_store_path_factory.cpp
347355
core/utils/file_utils.cpp
348356
core/utils/manifest_meta_reader.cpp
@@ -737,6 +745,7 @@ if(PAIMON_BUILD_TESTS)
737745
core/utils/consumer_manager_test.cpp
738746
core/utils/file_store_path_factory_cache_test.cpp
739747
core/utils/field_mapping_test.cpp
748+
core/utils/nested_projection_utils_test.cpp
740749
core/utils/file_store_path_factory_test.cpp
741750
core/utils/file_utils_test.cpp
742751
core/utils/manifest_meta_reader_test.cpp

src/paimon/common/data/shredding/map_shared_shredding_context.cpp

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "paimon/common/data/shredding/map_shared_shredding_context.h"
1818

1919
#include <algorithm>
20+
#include <cmath>
2021

2122
namespace paimon {
2223

@@ -32,8 +33,8 @@ std::map<std::string, int32_t> MapSharedShreddingContext::ComputeNextK() const {
3233
// First file — no history, use K_max.
3334
result[field_name] = k_max;
3435
} else {
35-
int32_t window_max = ComputeWindowMax(it->second);
36-
result[field_name] = std::max(1, std::min(window_max, k_max));
36+
int32_t adaptive_width = ComputeAdaptiveWidth(it->second);
37+
result[field_name] = std::max(1, std::min(adaptive_width, k_max));
3738
}
3839
}
3940
return result;
@@ -57,12 +58,27 @@ std::vector<std::string> MapSharedShreddingContext::GetShreddingColumnNames() co
5758
return names;
5859
}
5960

60-
int32_t MapSharedShreddingContext::ComputeWindowMax(const std::vector<int32_t>& values) {
61+
int32_t MapSharedShreddingContext::ComputeAdaptiveWidth(const std::vector<int32_t>& values) {
6162
if (values.empty()) {
6263
return 0;
6364
}
64-
// TODO(xinyu.lxy): support P99
65-
return *std::max_element(values.begin(), values.end());
65+
66+
std::vector<int32_t> sorted_values(values.begin(), values.end());
67+
std::sort(sorted_values.begin(), sorted_values.end());
68+
69+
int32_t max_width = sorted_values.back();
70+
auto percentile_rank = static_cast<int64_t>(std::ceil(kPercentileRatio * sorted_values.size()));
71+
percentile_rank = std::clamp<int64_t>(percentile_rank, 1, sorted_values.size());
72+
int32_t percentile_width = sorted_values[percentile_rank - 1];
73+
74+
// Use P90 to ignore far outliers, but keep max when it is close enough to normal rows.
75+
auto relative_close_threshold = static_cast<int32_t>(
76+
std::ceil(static_cast<double>(percentile_width) * kMaxCloseRelativeRatio));
77+
if (max_width - percentile_width <= kMaxCloseAbsoluteSlack ||
78+
max_width <= relative_close_threshold) {
79+
return max_width;
80+
}
81+
return percentile_width;
6682
}
6783

6884
} // namespace paimon

src/paimon/common/data/shredding/map_shared_shredding_context.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ namespace paimon {
3030
/// values to support adaptive K sizing across files.
3131
///
3232
/// - First file: K = K_max (no history).
33-
/// - Subsequent files: K = min(max(recent_max_row_widths), K_max).
33+
/// - Subsequent files: K = min(adaptive_width(recent_max_row_widths), K_max).
3434
class MapSharedShreddingContext {
3535
public:
3636
/// @param column_to_k_max Map from field name to its K_max (from options).
@@ -50,9 +50,12 @@ class MapSharedShreddingContext {
5050
std::vector<std::string> GetShreddingColumnNames() const;
5151

5252
private:
53-
static constexpr int32_t kWindowSize = 100;
53+
static constexpr int32_t kWindowSize = 20;
54+
static constexpr double kPercentileRatio = 0.90;
55+
static constexpr int32_t kMaxCloseAbsoluteSlack = 4;
56+
static constexpr double kMaxCloseRelativeRatio = 1.25;
5457

55-
static int32_t ComputeWindowMax(const std::vector<int32_t>& values);
58+
static int32_t ComputeAdaptiveWidth(const std::vector<int32_t>& values);
5659

5760
/// K_max per shared-shredding field, from options.
5861
std::map<std::string, int32_t> column_to_k_max_;

src/paimon/common/data/shredding/map_shared_shredding_context_test.cpp

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ TEST(MapSharedShreddingContextTest, FirstFileUsesKMax) {
3737

3838
TEST(MapSharedShreddingContextTest, AdaptKAfterOneFile) {
3939
// After reporting stats from one file, K should adapt to
40-
// min(max_row_width, K_max).
40+
// min(adaptive width, K_max).
4141
std::map<std::string, int32_t> field_to_k_max = {{"m", 10}};
4242
MapSharedShreddingContext context(field_to_k_max);
4343

@@ -64,20 +64,71 @@ TEST(MapSharedShreddingContextTest, AdaptKCappedByKMax) {
6464
ASSERT_EQ(5, next_k.at("m"));
6565
}
6666

67-
TEST(MapSharedShreddingContextTest, WindowMaxTracksLargest) {
68-
// K should use the max of all recent max_row_widths within the window.
67+
TEST(MapSharedShreddingContextTest, WindowP90UsesMaxWhenSamplesAreClose) {
68+
// Small sample windows still use max because max and P90 are close.
6969
std::map<std::string, int32_t> field_to_k_max = {{"m", 20}};
7070
MapSharedShreddingContext context(field_to_k_max);
7171

7272
context.ReportFileStats("m", 3);
7373
context.ReportFileStats("m", 7);
7474
context.ReportFileStats("m", 5);
7575

76-
// max of {3, 7, 5} = 7, capped by K_max=20 → K=7.
7776
auto next_k = context.ComputeNextK();
7877
ASSERT_EQ(7, next_k.at("m"));
7978
}
8079

80+
TEST(MapSharedShreddingContextTest, WindowP90IgnoresSingleFarOutlier) {
81+
std::map<std::string, int32_t> field_to_k_max = {{"m", 2000}};
82+
MapSharedShreddingContext context(field_to_k_max);
83+
84+
for (int32_t i = 0; i < 19; ++i) {
85+
context.ReportFileStats("m", 3);
86+
}
87+
context.ReportFileStats("m", 1000);
88+
89+
auto next_k = context.ComputeNextK();
90+
ASSERT_EQ(3, next_k.at("m"));
91+
}
92+
93+
TEST(MapSharedShreddingContextTest, WindowP90UsesMaxWithinAbsoluteSlack) {
94+
std::map<std::string, int32_t> field_to_k_max = {{"m", 20}};
95+
MapSharedShreddingContext context(field_to_k_max);
96+
97+
for (int32_t i = 0; i < 19; ++i) {
98+
context.ReportFileStats("m", 3);
99+
}
100+
context.ReportFileStats("m", 7);
101+
102+
auto next_k = context.ComputeNextK();
103+
ASSERT_EQ(7, next_k.at("m"));
104+
}
105+
106+
TEST(MapSharedShreddingContextTest, WindowP90UsesMaxWithinRelativeSlack) {
107+
std::map<std::string, int32_t> field_to_k_max = {{"m", 200}};
108+
MapSharedShreddingContext context(field_to_k_max);
109+
110+
for (int32_t i = 0; i < 19; ++i) {
111+
context.ReportFileStats("m", 100);
112+
}
113+
context.ReportFileStats("m", 125);
114+
115+
auto next_k = context.ComputeNextK();
116+
ASSERT_EQ(125, next_k.at("m"));
117+
}
118+
119+
TEST(MapSharedShreddingContextTest, WindowP90IgnoresMaxBeyondBothSlacks) {
120+
std::map<std::string, int32_t> field_to_k_max = {{"m", 200}};
121+
MapSharedShreddingContext context(field_to_k_max);
122+
123+
for (int32_t i = 0; i < 19; ++i) {
124+
context.ReportFileStats("m", 100);
125+
}
126+
context.ReportFileStats("m", 130);
127+
128+
auto next_k = context.ComputeNextK();
129+
ASSERT_EQ(100, next_k.at("m"));
130+
}
131+
81132
TEST(MapSharedShreddingContextTest, MultipleColumnsIndependent) {
82133
// Each field adapts independently.
83134
std::map<std::string, int32_t> field_to_k_max = {{"tags", 10}, {"attrs", 6}};
@@ -101,8 +152,7 @@ TEST(MapSharedShreddingContextTest, MultipleColumnsIndependent) {
101152
context.ReportFileStats("attrs", 6);
102153

103154
auto k3 = context.ComputeNextK();
104-
// tags: max(4,8)=8, capped by 10 → 8
105-
// attrs: max(2,6)=6, capped by 6 → 6
155+
// tags and attrs are close sample windows, so adaptive width keeps max.
106156
ASSERT_EQ(8, k3.at("tags"));
107157
ASSERT_EQ(6, k3.at("attrs"));
108158
}
@@ -116,7 +166,7 @@ TEST(MapSharedShreddingContextTest, GetShreddingColumnNames) {
116166
}
117167

118168
TEST(MapSharedShreddingContextTest, SlidingWindowEvictsOldEntries) {
119-
// The window size is 100. After filling 100 entries, adding one more
169+
// The window size is 20. After filling 20 entries, adding one more
120170
// should evict the oldest. Verify that the evicted value no longer
121171
// affects ComputeNextK.
122172
std::map<std::string, int32_t> field_to_k_max = {{"m", 256}};
@@ -125,19 +175,19 @@ TEST(MapSharedShreddingContextTest, SlidingWindowEvictsOldEntries) {
125175
// Insert a large value as the first entry.
126176
context.ReportFileStats("m", 200);
127177

128-
// Fill the remaining 99 slots with small values.
129-
for (int i = 0; i < 99; ++i) {
178+
// Fill the remaining 19 slots with small values.
179+
for (int32_t i = 0; i < 19; ++i) {
130180
context.ReportFileStats("m", 3);
131181
}
132182

133-
// Window = [200, 3, 3, ..., 3] (100 entries). Max = 200.
183+
// Window = [200, 3, 3, ..., 3] (20 entries). P90 = 3, max is a far outlier.
134184
auto k_before = context.ComputeNextK();
135-
ASSERT_EQ(200, k_before.at("m"));
185+
ASSERT_EQ(3, k_before.at("m"));
136186

137187
// Push one more — evicts the 200.
138188
context.ReportFileStats("m", 5);
139189

140-
// Window = [3, 3, ..., 3, 5] (100 entries). Max = 5.
190+
// Window = [3, 3, ..., 3, 5] (20 entries). Max is within the absolute slack.
141191
auto k_after = context.ComputeNextK();
142192
ASSERT_EQ(5, k_after.at("m"));
143193
}

0 commit comments

Comments
 (0)