Skip to content

Commit cf44831

Browse files
committed
[core] Remap predicate field index to projected read schema in InternalReadContext
Predicates are constructed against the latest table schema, so each LeafPredicate carries a field index that points to the column's position in the full table schema. When the query projects a subset of columns the read_schema built inside InternalReadContext::Create lays those columns out at different positions; the leaf's field index no longer matches the column it names. The strict field-id validation that runs immediately afterwards then fails: Paimon TableRead::Create error: Invalid: field obs_index has field idx 0 in input schema, mismatch field idx 1 in predicate The downstream LeafPredicateImpl::Test paths use field_index_ directly to index into arrow arrays / internal rows built from read_schema, so even if validation were relaxed, leaving the mismatch in place would silently read the wrong column. Walk the predicate tree once at context construction and rebuild each LeafPredicate with the field index resolved from the read_schema by field name. CompoundPredicate is reconstructed only when any descendant actually changed, otherwise the original shared_ptr is reused. GetPredicate() now returns the remapped predicate; the original (with latest-schema indices) is still available via the wrapped ReadContext if ever needed. CreateWithSchema lays the context over a different read_schema (e.g. the minimal column set for COUNT(*)), so it also remaps from the original FE predicate against the new read_schema rather than copying the already-remapped one whose indices are aligned with the original read_schema. Repro: -- DE table with btree global index on obs_index CREATE TABLE t ( clip_id STRING, obs_index INT, time_offset_ms BIGINT, collected_date DATE ) PARTITIONED BY (collected_date, clip_id) TBLPROPERTIES ( 'data-evolution.enabled' = 'true', 'global-index.btree.index-column' = 'obs_index', 'bucket' = '-1' ); -- after INSERT + CALL paimon.sys.create_global_index(...) SELECT clip_id, obs_index, time_offset_ms FROM t WHERE collected_date = '2026-05-26' AND clip_id = 'clip_a' AND obs_index BETWEEN 0 AND 10; -- before: TableRead::Create error (field idx mismatch) -- after: returns matching rows Coverage: InternalReadContext gains four cases — TestPredicateFieldIdxRemappedWhenProjected, TestPredicateUnchangedWhenAligned, TestCompoundPredicateRemap, TestPredicateOnFieldMissingFromReadSchema — covering the projected/aligned/compound/missing-field paths through the remap. Signed-off-by: duanyyyyyyy <yan.duan9759@gmail.com>
1 parent 2e79a5b commit cf44831

8 files changed

Lines changed: 326 additions & 33 deletions

File tree

include/paimon/predicate/leaf_predicate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ enum class FieldType;
3232
/// Leaf node of a `Predicate` tree. Compares a field with literals.
3333
class PAIMON_EXPORT LeafPredicate : virtual public Predicate {
3434
public:
35+
/// The field's position in the schema this predicate is currently bound to.
36+
/// At construction the value reflects the schema the caller supplied to
37+
/// `PredicateBuilder`; predicates obtained from `InternalReadContext::GetPredicate()`
38+
/// have already been projected onto the read schema (see
39+
/// `InternalReadContext::GetPredicate()` for the projection semantics).
3540
int32_t FieldIndex() const {
3641
return field_index_;
3742
}

include/paimon/predicate/predicate_builder.h

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,28 @@ enum class FieldType;
3232
///
3333
/// PredicateBuilder provides static factory methods to create various types of predicates
3434
/// that can be used for filtering data in Paimon tables.
35+
///
36+
/// The `field_index` parameter accepted by every factory method is the position of the
37+
/// field in the schema the caller is working with — typically the latest table schema.
38+
/// When the resulting predicate is later attached to an `InternalReadContext`,
39+
/// `InternalReadContext::Create` projects each leaf onto the read schema (mirrors
40+
/// paimon Java's `PredicateProjectionConverter`): leaf field indices are rewritten via
41+
/// the table-schema → read-schema position mapping (keyed by the stable paimon field
42+
/// id, so it survives column renames), and leaves / OR branches whose fields are not
43+
/// in the projection are dropped. Callers therefore do not need to track projection
44+
/// state themselves. `field_name` is informational (used for debug / display) and
45+
/// does not participate in projection lookup.
3546
class PAIMON_EXPORT PredicateBuilder {
3647
public:
3748
PredicateBuilder() = delete;
3849
~PredicateBuilder() = delete;
3950

4051
/// Create an equality predicate (field == literal).
4152
///
42-
/// @param field_index The index of the field in read schema (0-based).
43-
/// @param field_name The name of the field.
53+
/// @param field_index The position of the field in the schema the caller is working
54+
/// with (0-based); projected onto the read schema by
55+
/// `InternalReadContext::Create`. See class doc for details.
56+
/// @param field_name The name of the field (informational; see class doc).
4457
/// @param field_type The data type of the field.
4558
/// @param literal The literal value to compare against.
4659
/// @return A shared pointer to the created Predicate object.
@@ -99,8 +112,10 @@ class PAIMON_EXPORT PredicateBuilder {
99112
///
100113
/// Tests whether the field value falls within the specified range (inclusive on both ends).
101114
///
102-
/// @param field_index The index of the field in read schema (0-based).
103-
/// @param field_name The name of the field.
115+
/// @param field_index The position of the field in the schema the caller is working
116+
/// with (0-based); projected onto the read schema by
117+
/// `InternalReadContext::Create`. See class doc for details.
118+
/// @param field_name The name of the field (informational; see class doc).
104119
/// @param field_type The data type of the field.
105120
/// @param included_lower_bound The lower bound of the range (inclusive).
106121
/// @param included_upper_bound The upper bound of the range (inclusive).

include/paimon/read_context.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ class PAIMON_EXPORT ReadContextBuilder {
206206
/// It can significantly improve performance by reducing the amount of data
207207
/// that needs to be read and processed.
208208
///
209+
/// The caller should construct the predicate against the latest table schema.
210+
/// `InternalReadContext::Create` projects each leaf onto the read schema
211+
/// (mirroring paimon Java's `PredicateProjectionConverter`): leaf field indices
212+
/// are rewritten to positions in the read schema, and AND children / OR branches
213+
/// whose fields are not in the projection are pruned. The predicate therefore
214+
/// does not need to be projection-aware.
215+
///
209216
/// @param predicate Shared pointer to the predicate for data filtering.
210217
/// @return Reference to this builder for method chaining.
211218
ReadContextBuilder& SetPredicate(const std::shared_ptr<Predicate>& predicate);

src/paimon/common/predicate/predicate_builder.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@
4242
namespace paimon {
4343
enum class FieldType;
4444

45-
// TODO(xinyu.lxy): predicate field_index use index in read schema now, but java paimon use index
46-
// in file schema
4745
std::shared_ptr<Predicate> PredicateBuilder::Equal(int32_t field_index,
4846
const std::string& field_name,
4947
const FieldType& field_type,

src/paimon/core/operation/internal_read_context.cpp

Lines changed: 135 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,124 @@
1616

1717
#include "paimon/core/operation/internal_read_context.h"
1818

19+
#include <optional>
1920
#include <utility>
2021

22+
#include "paimon/common/predicate/compound_predicate_impl.h"
23+
#include "paimon/common/predicate/leaf_predicate_impl.h"
2124
#include "paimon/common/predicate/predicate_validator.h"
2225
#include "paimon/common/table/special_fields.h"
2326
#include "paimon/common/types/data_field.h"
2427
#include "paimon/core/schema/arrow_schema_validator.h"
28+
#include "paimon/predicate/function.h"
2529
#include "paimon/status.h"
2630

2731
namespace arrow {
2832
class Schema;
2933
} // namespace arrow
3034

3135
namespace paimon {
36+
namespace {
37+
// Build a map from a field's position in `table_schema` (the latest table schema, the
38+
// index space upstream predicates are typically constructed against) to its position
39+
// in `read_data_fields` (the projected read schema). Field identity is the stable
40+
// field id, so the mapping survives column renames within the same schema. Read-only
41+
// special fields (RowId / SequenceNumber / etc.) have no analogue in the table
42+
// schema and are skipped — user-supplied predicates do not reference them.
43+
std::map<int32_t, int32_t> BuildLatestToReadIdxMapping(
44+
const TableSchema& table_schema, const std::vector<DataField>& read_data_fields) {
45+
std::map<int32_t, int32_t> id_to_latest_idx;
46+
const auto& table_fields = table_schema.Fields();
47+
for (size_t latest_idx = 0; latest_idx < table_fields.size(); latest_idx++) {
48+
id_to_latest_idx[table_fields[latest_idx].Id()] = static_cast<int32_t>(latest_idx);
49+
}
50+
std::map<int32_t, int32_t> mapping;
51+
for (size_t read_idx = 0; read_idx < read_data_fields.size(); read_idx++) {
52+
auto iter = id_to_latest_idx.find(read_data_fields[read_idx].Id());
53+
if (iter != id_to_latest_idx.end()) {
54+
mapping[iter->second] = static_cast<int32_t>(read_idx);
55+
}
56+
}
57+
return mapping;
58+
}
59+
60+
// Project `predicate` onto the read schema, rewriting each leaf's field index via
61+
// `latest_to_read_idx` (predicate's source index space → read schema position).
62+
//
63+
// Inclusive semantics, matching paimon Java's PredicateProjectionConverter:
64+
// - Leaf whose field is not in the read schema: dropped (returns nullopt).
65+
// - AND: drop non-projectable children, keep the rest. Safe because if `A AND B`
66+
// holds for a row, A holds too; the projected predicate is a superset
67+
// (necessary, not sufficient).
68+
// - OR: every child must be projectable. If any child is dropped, drop the
69+
// whole OR — otherwise a row that only satisfied the dropped branch would
70+
// falsely pass.
71+
// - Predicates without a field index (e.g. full-text / vector search) flow
72+
// through unchanged.
73+
Result<std::optional<std::shared_ptr<Predicate>>> ProjectPredicate(
74+
const std::map<int32_t, int32_t>& latest_to_read_idx,
75+
const std::shared_ptr<Predicate>& predicate) {
76+
if (auto leaf = std::dynamic_pointer_cast<LeafPredicateImpl>(predicate)) {
77+
auto iter = latest_to_read_idx.find(leaf->FieldIndex());
78+
if (iter == latest_to_read_idx.end()) {
79+
return std::optional<std::shared_ptr<Predicate>>{};
80+
}
81+
if (iter->second == leaf->FieldIndex()) {
82+
return std::optional<std::shared_ptr<Predicate>>{predicate};
83+
}
84+
return std::optional<std::shared_ptr<Predicate>>{
85+
std::static_pointer_cast<Predicate>(leaf->NewLeafPredicate(iter->second))};
86+
}
87+
if (auto compound = std::dynamic_pointer_cast<CompoundPredicateImpl>(predicate)) {
88+
const bool is_and = compound->GetFunction().GetType() == Function::Type::AND;
89+
std::vector<std::shared_ptr<Predicate>> projected_children;
90+
projected_children.reserve(compound->Children().size());
91+
bool any_changed = false;
92+
for (const auto& child : compound->Children()) {
93+
PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<Predicate>> projected_child,
94+
ProjectPredicate(latest_to_read_idx, child));
95+
if (!projected_child.has_value()) {
96+
if (!is_and) {
97+
return std::optional<std::shared_ptr<Predicate>>{};
98+
}
99+
any_changed = true;
100+
continue;
101+
}
102+
if (projected_child.value() != child) {
103+
any_changed = true;
104+
}
105+
projected_children.push_back(std::move(projected_child.value()));
106+
}
107+
if (projected_children.empty()) {
108+
return std::optional<std::shared_ptr<Predicate>>{};
109+
}
110+
if (projected_children.size() == 1) {
111+
return std::optional<std::shared_ptr<Predicate>>{std::move(projected_children[0])};
112+
}
113+
if (!any_changed) {
114+
return std::optional<std::shared_ptr<Predicate>>{predicate};
115+
}
116+
return std::optional<std::shared_ptr<Predicate>>{std::static_pointer_cast<Predicate>(
117+
compound->NewCompoundPredicate(projected_children))};
118+
}
119+
return std::optional<std::shared_ptr<Predicate>>{predicate};
120+
}
121+
122+
Result<std::shared_ptr<Predicate>> ProjectAndValidatePredicate(
123+
const arrow::Schema& read_schema, const std::map<int32_t, int32_t>& latest_to_read_idx,
124+
const std::shared_ptr<Predicate>& predicate) {
125+
PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<Predicate>> projected,
126+
ProjectPredicate(latest_to_read_idx, predicate));
127+
if (!projected.has_value()) {
128+
return std::shared_ptr<Predicate>{};
129+
}
130+
PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithSchema(
131+
read_schema, projected.value(), /*validate_field_idx=*/true));
132+
PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithLiterals(projected.value()));
133+
return projected.value();
134+
}
135+
} // namespace
136+
32137
Result<std::unique_ptr<InternalReadContext>> InternalReadContext::Create(
33138
const std::shared_ptr<ReadContext>& context, const std::shared_ptr<TableSchema>& table_schema,
34139
const std::map<std::string, std::string>& options) {
@@ -95,34 +200,51 @@ Result<std::unique_ptr<InternalReadContext>> InternalReadContext::Create(
95200
auto read_schema = DataField::ConvertDataFieldsToArrowSchema(read_data_fields);
96201
// validate read schema to avoid redundant fields
97202
PAIMON_RETURN_NOT_OK(ArrowSchemaValidator::ValidateSchemaWithFieldId(*read_schema));
98-
// validate predicate
203+
// Project the upstream predicate onto `read_schema`. Predicates carry field indices
204+
// pointing into the latest table schema; rewrite them to positions in `read_schema_`
205+
// so downstream readers can apply them directly.
206+
std::shared_ptr<Predicate> projected_predicate;
99207
if (context->GetPredicate()) {
100-
PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithSchema(
101-
*read_schema, context->GetPredicate(), /*validate_field_idx=*/true));
102-
PAIMON_RETURN_NOT_OK(
103-
PredicateValidator::ValidatePredicateWithLiterals(context->GetPredicate()));
208+
auto latest_to_read_idx = BuildLatestToReadIdxMapping(*table_schema, read_data_fields);
209+
PAIMON_ASSIGN_OR_RAISE(
210+
projected_predicate,
211+
ProjectAndValidatePredicate(*read_schema, latest_to_read_idx, context->GetPredicate()));
104212
}
105213

106-
return std::unique_ptr<InternalReadContext>(
107-
new InternalReadContext(context, table_schema, read_schema, core_options));
214+
return std::unique_ptr<InternalReadContext>(new InternalReadContext(
215+
context, table_schema, read_schema, core_options, std::move(projected_predicate)));
108216
}
109217

110218
InternalReadContext::InternalReadContext(const std::shared_ptr<ReadContext>& read_context,
111219
const std::shared_ptr<TableSchema>& table_schema,
112220
const std::shared_ptr<arrow::Schema>& read_schema,
113-
const CoreOptions& options)
221+
const CoreOptions& options,
222+
std::shared_ptr<Predicate> projected_predicate)
114223
: read_context_(read_context),
115224
table_schema_(table_schema),
116225
read_schema_(read_schema),
117-
options_(options) {}
226+
options_(options),
227+
projected_predicate_(std::move(projected_predicate)) {}
118228

119229
Result<std::shared_ptr<InternalReadContext>> InternalReadContext::CreateWithSchema(
120230
const std::shared_ptr<InternalReadContext>& original,
121231
const std::shared_ptr<arrow::Schema>& new_read_schema) {
122-
// Create a new InternalReadContext sharing all properties except read_schema.
123-
// The new read_schema is the minimal column set for COUNT(*).
124-
return std::shared_ptr<InternalReadContext>(new InternalReadContext(
125-
original->read_context_, original->table_schema_, new_read_schema, original->options_));
232+
// The wrapped read_context still holds the caller-supplied predicate (in the latest
233+
// table schema's index space). Re-project it against `new_read_schema` directly,
234+
// rather than re-projecting the already-projected predicate sitting on `original`.
235+
std::shared_ptr<Predicate> projected_predicate;
236+
if (original->read_context_->GetPredicate()) {
237+
PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> new_read_data_fields,
238+
DataField::ConvertArrowSchemaToDataFields(new_read_schema));
239+
auto latest_to_read_idx =
240+
BuildLatestToReadIdxMapping(*original->table_schema_, new_read_data_fields);
241+
PAIMON_ASSIGN_OR_RAISE(projected_predicate, ProjectAndValidatePredicate(
242+
*new_read_schema, latest_to_read_idx,
243+
original->read_context_->GetPredicate()));
244+
}
245+
return std::shared_ptr<InternalReadContext>(
246+
new InternalReadContext(original->read_context_, original->table_schema_, new_read_schema,
247+
original->options_, std::move(projected_predicate)));
126248
}
127249

128250
} // namespace paimon

src/paimon/core/operation/internal_read_context.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,16 @@ class InternalReadContext {
6262
const std::vector<std::string>& GetPrimaryKeys() const {
6363
return table_schema_->PrimaryKeys();
6464
}
65+
// Returns the predicate projected onto `read_schema_`. Upstream constructs predicates
66+
// against the latest table schema, so when the query projects a subset of columns
67+
// the leaf field indices no longer match the projected schema. The projection is
68+
// done once at context construction (mirrors paimon Java's
69+
// `PredicateProjectionConverter`): leaf indices are rewritten via the table-schema
70+
// → read-schema mapping, AND children whose fields are absent from the read schema
71+
// are dropped (inclusive), and OR is dropped wholesale if any of its children is not
72+
// projectable. May be nullptr if the entire predicate is non-projectable.
6573
const std::shared_ptr<Predicate>& GetPredicate() const {
66-
return read_context_->GetPredicate();
74+
return projected_predicate_;
6775
}
6876
bool EnablePredicateFilter() const {
6977
return read_context_->EnablePredicateFilter();
@@ -110,12 +118,13 @@ class InternalReadContext {
110118
InternalReadContext(const std::shared_ptr<ReadContext>& read_context,
111119
const std::shared_ptr<TableSchema>& table_schema,
112120
const std::shared_ptr<arrow::Schema>& read_schema,
113-
const CoreOptions& options);
121+
const CoreOptions& options, std::shared_ptr<Predicate> projected_predicate);
114122

115123
std::shared_ptr<ReadContext> read_context_;
116124
std::shared_ptr<TableSchema> table_schema_;
117125
std::shared_ptr<arrow::Schema> read_schema_;
118126
CoreOptions options_;
127+
std::shared_ptr<Predicate> projected_predicate_;
119128
};
120129

121130
} // namespace paimon

0 commit comments

Comments
 (0)