Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;

import javax.annotation.Nullable;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;

/** A reference to a field in an input. */
Expand All @@ -34,19 +37,31 @@ public class FieldRef implements Serializable {
private static final String FIELD_INDEX = "index";
private static final String FIELD_NAME = "name";
private static final String FIELD_TYPE = "type";
private static final String FIELD_NESTED_INDEXES = "nestedIndexes";
private static final String FIELD_NESTED_ARITIES = "nestedArities";

private final int index;
private final String name;
private final DataType type;
@Nullable private final int[] nestedIndexes;
@Nullable private final int[] nestedArities;

public FieldRef(int index, String name, DataType type) {
this(index, name, type, null, null);
}

@JsonCreator
public FieldRef(
@JsonProperty(FIELD_INDEX) int index,
@JsonProperty(FIELD_NAME) String name,
@JsonProperty(FIELD_TYPE) DataType type) {
@JsonProperty(FIELD_TYPE) DataType type,
@JsonProperty(FIELD_NESTED_INDEXES) @Nullable int[] nestedIndexes,
@JsonProperty(FIELD_NESTED_ARITIES) @Nullable int[] nestedArities) {
this.index = index;
this.name = name;
this.type = type;
this.nestedIndexes = nestedIndexes;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because FieldRef now carries nestedIndexes / nestedArities, every place that remaps a FieldRef has to preserve them. Existing remappers such as PredicateProjectionConverter, PartitionValuePredicateVisitor, and TableQueryAuthResult still rebuild refs with the 3-arg constructor, so a pushed predicate like a.b = 1 loses its nested path after projection/auth/partition remapping. The resulting FieldTransform then reads top-level a as the leaf type instead of traversing to b, which can produce wrong filtering or runtime failures. Please add a copy/remap helper that preserves the nested metadata and use it for all FieldRef rewrites before enabling nested predicates.

this.nestedArities = nestedArities;
}

@JsonProperty(FIELD_INDEX)
Expand All @@ -64,6 +79,38 @@ public DataType type() {
return type;
}

@JsonProperty(FIELD_NESTED_INDEXES)
@Nullable
public int[] nestedIndexes() {
return nestedIndexes;
}

@JsonProperty(FIELD_NESTED_ARITIES)
@Nullable
public int[] nestedArities() {
return nestedArities;
}

/**
* Returns a copy of this ref with the given top-level index, preserving name, type and nested
* path metadata.
*/
public FieldRef withIndex(int newIndex) {
return new FieldRef(newIndex, name, type, nestedIndexes, nestedArities);
}

/**
* Returns the name of the top-level field this ref points at: {@link #name()} for a top-level
* ref, the first path segment for a nested ref (whose name is the full dotted path).
*/
public String topLevelName() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new helper also needs to be reflected in the field-name collection paths. PredicateVisitor.collectFieldNames() still collects ref.name(), which is now the full nested path (s.b) for these Spark predicates. One concrete fallout is AbstractDataTableRead.authedReader: when a user projects only id but the auth row filter references s.b, the pre-read expansion checks the collected names against top-level table fields, does not add s, and TableQueryAuthResult then rejects the filter because the output row type lacks the top-level s column. Please either make the collector return top-level names for FieldRefs (or add/use a top-level collector at those call sites) and add a regression where auth filtering on a nested field works even when the user projection excludes that top-level field.

if (nestedIndexes == null || nestedIndexes.length == 0) {
return name;
}
int firstDot = name.indexOf('.');
return firstDot < 0 ? name : name.substring(0, firstDot);
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -75,12 +122,17 @@ public boolean equals(Object o) {
FieldRef fieldRef = (FieldRef) o;
return index == fieldRef.index
&& Objects.equals(name, fieldRef.name)
&& Objects.equals(type, fieldRef.type);
&& Objects.equals(type, fieldRef.type)
&& Arrays.equals(nestedIndexes, fieldRef.nestedIndexes)
&& Arrays.equals(nestedArities, fieldRef.nestedArities);
}

@Override
public int hashCode() {
return Objects.hash(index, name, type);
int result = Objects.hash(index, name, type);
result = 31 * result + Arrays.hashCode(nestedIndexes);
result = 31 * result + Arrays.hashCode(nestedArities);
return result;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,34 @@ public DataType outputType() {

@Override
public Object transform(InternalRow row) {
return get(row, fieldRef.index(), fieldRef.type());
if (fieldRef.nestedIndexes() == null || fieldRef.nestedIndexes().length == 0) {
return get(row, fieldRef.index(), fieldRef.type());
}

InternalRow currentRow = row;
if (currentRow == null) {
return null;
}
int[] indexes = fieldRef.nestedIndexes();
int[] arities = fieldRef.nestedArities();

if (currentRow.isNullAt(fieldRef.index())) {
return null;
}
currentRow = currentRow.getRow(fieldRef.index(), arities[0]);

for (int i = 0; i < indexes.length - 1; i++) {
if (currentRow == null || currentRow.isNullAt(indexes[i])) {
return null;
}
currentRow = currentRow.getRow(indexes[i], arities[i + 1]);
}

if (currentRow == null) {
return null;
}
int leafIndex = indexes[indexes.length - 1];
return get(currentRow, leafIndex, fieldRef.type());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ public boolean test(
return !(function instanceof AlwaysFalse);
}
FieldRef fieldRef = fieldRefOptional.get();
if (fieldRef.nestedIndexes() != null && fieldRef.nestedIndexes().length > 0) {
return true;
}
int index = fieldRef.index();
DataType type = fieldRef.type();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public Optional<Predicate> visit(LeafPredicate predicate) {
if (input instanceof FieldRef) {
FieldRef ref = (FieldRef) input;
int partIdx = tableToPartitionMapping[ref.index()];
remappedInputs.add(new FieldRef(partIdx, ref.name(), ref.type()));
remappedInputs.add(ref.withIndex(partIdx));
} else {
remappedInputs.add(input);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ public Predicate in(int idx, List<Object> literals) {
public Predicate in(Transform transform, List<Object> literals) {
// In the IN predicate, 20 literals are critical for performance.
// If there are more than 20 literals, the performance will decrease.
if (literals.size() > 20) {
if (literals.size() > 20 || literals.isEmpty()) {
return LeafPredicate.of(transform, In.INSTANCE, literals);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public Optional<Predicate> visit(LeafPredicate predicate) {
FieldRef fieldRef = (FieldRef) input;
Integer mappedIndex = reversed.get(fieldRef.index());
if (mappedIndex != null) {
newInputs.add(new FieldRef(mappedIndex, fieldRef.name(), fieldRef.type()));
newInputs.add(fieldRef.withIndex(mappedIndex));
} else {
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.
*/

package org.apache.paimon.predicate;

import org.apache.paimon.types.DataTypes;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

/** Tests for {@link FieldRef}. */
class FieldRefTest {

@Test
void testTopLevelName() {
FieldRef topLevel = new FieldRef(0, "a.b", DataTypes.INT());
assertThat(topLevel.topLevelName()).isEqualTo("a.b");

FieldRef nested = new FieldRef(0, "a.b", DataTypes.INT(), new int[] {1}, new int[] {2});
assertThat(nested.topLevelName()).isEqualTo("a");

FieldRef deepNested =
new FieldRef(0, "a.b.c", DataTypes.INT(), new int[] {1, 0}, new int[] {2, 3});
assertThat(deepNested.topLevelName()).isEqualTo("a");
}

@Test
void testWithIndexPreservesNestedMetadata() {
FieldRef nested = new FieldRef(3, "a.b", DataTypes.INT(), new int[] {1}, new int[] {2});
FieldRef remapped = nested.withIndex(0);
assertThat(remapped.index()).isEqualTo(0);
assertThat(remapped.name()).isEqualTo("a.b");
assertThat(remapped.type()).isEqualTo(DataTypes.INT());
assertThat(remapped.nestedIndexes()).containsExactly(1);
assertThat(remapped.nestedArities()).containsExactly(2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,15 @@ private static Map<Integer, Transform> transformRemapping(
for (Object input : transform.inputs()) {
if (input instanceof FieldRef) {
FieldRef ref = (FieldRef) input;
int newIndex = outputRowType.getFieldIndex(ref.name());
int newIndex = outputRowType.getFieldIndex(ref.topLevelName());
if (newIndex < 0) {
throw new IllegalArgumentException(
"Column masking refers to field '"
+ ref.name()
+ "' which is not present in output row type "
+ outputRowType);
}
DataType type = outputRowType.getTypeAt(newIndex);
newInputs.add(new FieldRef(newIndex, ref.name(), type));
newInputs.add(ref.withIndex(newIndex));
} else {
newInputs.add(input);
}
Expand All @@ -221,16 +220,14 @@ public Predicate visit(LeafPredicate predicate) {
for (Object input : transform.inputs()) {
if (input instanceof FieldRef) {
FieldRef ref = (FieldRef) input;
String fieldName = ref.name();
int newIndex = outputRowType.getFieldIndex(fieldName);
int newIndex = outputRowType.getFieldIndex(ref.topLevelName());
if (newIndex < 0) {
throw new RuntimeException(
String.format(
"Unable to read data without column %s when row filter enabled.",
fieldName));
ref.name()));
}
DataType type = outputRowType.getTypeAt(newIndex);
newInputs.add(new FieldRef(newIndex, fieldName, type));
newInputs.add(ref.withIndex(newIndex));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still derives newIndex from ref.name() above, but ref.name() is now the full nested path for Spark nested predicates, for example a.b, while outputRowType only contains top-level fields. As a result, a valid nested row filter is rejected unless there happens to be a top-level column literally named a.b; the same lookup pattern appears in the masking remap above. Please remap by the top-level field (or carry the top-level name/index separately) and preserve the nested metadata when creating the new FieldRef.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks sir. for your reviews improved me

} else {
newInputs.add(input);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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.
*/

package org.apache.paimon.catalog;

import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.predicate.Equal;
import org.apache.paimon.predicate.FieldRef;
import org.apache.paimon.predicate.FieldTransform;
import org.apache.paimon.predicate.LeafPredicate;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.predicate.Transform;
import org.apache.paimon.types.DataType;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.IteratorRecordReader;
import org.apache.paimon.utils.JsonSerdeUtil;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

/** Tests for {@link TableQueryAuthResult}. */
class TableQueryAuthResultTest {

private static final RowType NESTED_TYPE =
RowType.of(
new DataType[] {DataTypes.INT(), DataTypes.STRING()}, new String[] {"b", "c"});

/** Output row type with top-level fields reordered relative to the table schema (id, s). */
private static final RowType OUTPUT_TYPE =
RowType.of(new DataType[] {NESTED_TYPE, DataTypes.INT()}, new String[] {"s", "id"});

private static Transform nestedFieldTransform() {
return new FieldTransform(
new FieldRef(1, "s.b", DataTypes.INT(), new int[] {0}, new int[] {2}));
}

@Test
void testNestedRowFilterRemappedByTopLevelField() throws Exception {
Predicate predicate =
LeafPredicate.of(
nestedFieldTransform(), Equal.INSTANCE, Collections.singletonList(10));
TableQueryAuthResult authResult =
new TableQueryAuthResult(
Collections.singletonList(JsonSerdeUtil.toJson(predicate)), null);

List<InternalRow> rows =
Arrays.asList(
GenericRow.of(GenericRow.of(10, BinaryString.fromString("x")), 1),
GenericRow.of(GenericRow.of(20, BinaryString.fromString("y")), 2));

List<Integer> ids = new ArrayList<>();
authResult
.doAuth(new IteratorRecordReader<>(rows.iterator()), OUTPUT_TYPE)
.forEachRemaining(row -> ids.add(row.getInt(1)));

assertThat(ids).containsExactly(1);
}

@Test
void testNestedColumnMaskingRemappedByTopLevelField() throws Exception {
Map<String, String> masking =
Collections.singletonMap("id", JsonSerdeUtil.toJson(nestedFieldTransform()));
TableQueryAuthResult authResult = new TableQueryAuthResult(null, masking);

List<InternalRow> rows =
Collections.singletonList(
GenericRow.of(GenericRow.of(42, BinaryString.fromString("x")), 1));

List<Integer> ids = new ArrayList<>();
authResult
.doAuth(new IteratorRecordReader<>(rows.iterator()), OUTPUT_TYPE)
.forEachRemaining(row -> ids.add(row.getInt(1)));

assertThat(ids).containsExactly(42);
}
}
Loading
Loading